]> 127.0.0.1 Git - bili_danmu/.git/commitdiff
Add fmp4快速索引文件 (#146)
authorqydysky <qydysky@foxmail.com>
Sun, 12 Jan 2025 18:13:24 +0000 (02:13 +0800)
committerGitHub <noreply@github.com>
Sun, 12 Jan 2025 18:13:24 +0000 (02:13 +0800)
* Add fmp4快速索引文件

* Fix golangci-lint

* Fix golangci-lint

README.md
Reply/F.go
Reply/F/comp.go
Reply/F/videoFastSeed/videoFastSeed.go [new file with mode: 0644]
Reply/fmp4Decode.go
Reply/fmp4Decode_test.go
Reply/stream.go

index 814f22ea83fe6826d61be3cc26a726842e27fe17..dde3534a24ae6f81f219252e24734d7b460f0119 100644 (file)
--- a/README.md
+++ b/README.md
@@ -163,6 +163,35 @@ Warning: Binary output can mess up your terminal. Use "--output -" to tell curl
 * closing connection #0
 ```
 
+添加快速索引文件生成,将在录制完成后,读取视频文件,并将关键帧的时间戳和对应的下标值记录在`.fastSeed`文件,用于加快后续切片请求响应。(>v0.14.28)
+
+相较于之前的请求时进行查找,效率提升如下(以mp4格式为例)
+```
+旧:
+// 10s-30s 110.962896ms
+// 10m-10m20s 1.955749395s
+// 30m-30m20s 5.791614855s
+
+新:
+// 10s-30s 90.05983ms
+// 10m-10m20s 88.769475ms
+// 30m-30m20s 104.381225ms
+```
+
+`.fastSeed`文件格式:
+```
++--------+-----------+
+|Ms int64|index int64|
++--------+-----------+
+|8bytes  |8bytes     |
++--------+-----------+
+
+Ms:毫秒时间戳
+index:相对于文件起始位置的byte下标值
+
+文件由n组上述16bytes组成,时间戳升序
+```
+
 #### Web自定义响应头
 配置文件中添加配置项`Web自定义响应头`(>v0.14.19)。默认为空,当不为空时,将在所有响应中添加指定头。
 例子:
index 0871382501808bfda76ec67b34aec0e3323ad4f9..d5547bd0e8c40307a4174c701e8dd8a37b4b1c09 100644 (file)
@@ -1214,8 +1214,17 @@ func init() {
                                                        if v, ok := c.C.K_v.LoadV(`fmp4音视频时间戳容差s`).(float64); ok && v > 0.1 {
                                                                fmp4Decoder.AVTDiff = v
                                                        }
-                                                       if e := fmp4Decoder.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 := fmp4Decoder.CutSeed(f, startT, duration, res, f, gf); e != nil && !errors.Is(e, io.EOF) {
+                                                                       flog.L(`E: `, e)
+                                                               }
+                                                       } else {
+                                                               if e := fmp4Decoder.Cut(f, startT, duration, res); e != nil && !errors.Is(e, io.EOF) {
+                                                                       flog.L(`E: `, e)
+                                                               }
                                                        }
                                                }
                                        } else if e := f.CopyToIoWriter(w, pio.CopyConfig{BytePerSec: speed, SkipByte: rangeHeaderNum}); e != nil {
index 7add94a535ef5a38115408e1094ad8dd2669f08e..3c4406e39aa76318f9adb201b104c63bd0f32aeb 100644 (file)
@@ -9,6 +9,7 @@ import (
        _ "github.com/qydysky/bili_danmu/Reply/F/danmuCountPerMin"
        _ "github.com/qydysky/bili_danmu/Reply/F/danmuEmotes"
        _ "github.com/qydysky/bili_danmu/Reply/F/danmuji"
+       _ "github.com/qydysky/bili_danmu/Reply/F/videoFastSeed"
        comp "github.com/qydysky/part/component2"
        log "github.com/qydysky/part/log"
 )
@@ -30,6 +31,11 @@ var Danmuji = comp.Get[interface {
        Danmuji_auto(ctx context.Context, danmus []any, waitSec float64, then func(string))
 }](`danmuji`)
 
+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`)
+
 type DanmuEmotesS struct {
        Logg *log.Log_interface
        Info []any
diff --git a/Reply/F/videoFastSeed/videoFastSeed.go b/Reply/F/videoFastSeed/videoFastSeed.go
new file mode 100644 (file)
index 0000000..3198844
--- /dev/null
@@ -0,0 +1,131 @@
+package videofastseed
+
+import (
+       "errors"
+       "os"
+       "time"
+
+       comp "github.com/qydysky/part/component2"
+       file "github.com/qydysky/part/file"
+)
+
+type TargetInterface 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)
+}
+
+func init() {
+       if e := comp.Register[TargetInterface]("videoFastSeed", videoFastSeed{}); e != nil {
+               panic(e)
+       }
+}
+
+var (
+       ErrFormat    = errors.New("ErrFormat")
+       ErrNoInitSav = errors.New("ErrNoInitSav")
+       ErrNoInitGet = errors.New("ErrNoInitGet")
+)
+
+type videoFastSeed struct {
+       initSav  bool
+       initGet  bool
+       filepath string
+}
+
+func (_ videoFastSeed) InitGet(fastSeedFilePath string) (getIndex func(seedTo time.Duration) (int64, error), e error) {
+       t := videoFastSeed{}
+       t.filepath = fastSeedFilePath
+       f := file.New(t.filepath, -1, false)
+       defer f.Close()
+       if !f.IsExist() {
+               return nil, os.ErrNotExist
+       }
+       t.initGet = true
+       return t.GetIndex, nil
+}
+
+func (_ videoFastSeed) InitSav(fastSeedFilePath string) (savIndex func(seedTo time.Duration, cuIndex int64) error, e error) {
+       t := videoFastSeed{}
+       t.filepath = fastSeedFilePath
+       f := file.New(t.filepath, -1, false)
+       defer f.Close()
+       if f.IsExist() {
+               _ = f.Delete()
+       }
+       t.initSav = true
+       return t.SavIndex, nil
+}
+
+func (t *videoFastSeed) SavIndex(ms time.Duration, cuIndex int64) error {
+       if !t.initSav {
+               return ErrNoInitSav
+       }
+       f := file.New(t.filepath, -1, false)
+       defer f.Close()
+       if _, e := f.Write(Itob64(ms.Milliseconds()), false); e != nil {
+               return e
+       }
+       if _, e := f.Write(Itob64(cuIndex), false); e != nil {
+               return e
+       }
+       return nil
+}
+
+func (t *videoFastSeed) GetIndex(seedTo time.Duration) (int64, error) {
+       if !t.initGet {
+               return -1, ErrNoInitGet
+       }
+       f := file.New(t.filepath, 0, false)
+       defer f.Close()
+       if !f.IsExist() {
+               return -1, os.ErrNotExist
+       }
+       buf := make([]byte, 16)
+       lastIndex := int64(0)
+       for {
+               if n, e := f.Read(buf); e != nil {
+                       return -1, e
+               } else if n != 16 {
+                       return -1, ErrFormat
+               } else {
+                       if ms := Btoi(buf[:8], 0, 8); ms > seedTo.Milliseconds() {
+                               return lastIndex, nil
+                       } else {
+                               lastIndex = Btoi(buf[8:], 0, 8)
+                       }
+               }
+       }
+}
+
+func Btoi(b []byte, offset int, size int) int64 {
+       if size > 8 {
+               panic("最大8位")
+       }
+
+       bu := make([]byte, 8)
+       l := len(b) - offset
+       if l > size {
+               l = size
+       }
+       for i := 0; i < size && i < l; i++ {
+               bu[i+8-size] = b[offset+i]
+       }
+
+       //binary.BigEndian.Uint64
+       return int64(uint64(bu[7]) | uint64(bu[6])<<8 | uint64(bu[5])<<16 | uint64(bu[4])<<24 |
+               uint64(bu[3])<<32 | uint64(bu[2])<<40 | uint64(bu[1])<<48 | uint64(bu[0])<<56)
+}
+
+func Itob64(v int64) []byte {
+       //binary.BigEndian.PutUint64
+       b := make([]byte, 8)
+       b[0] = byte(v >> 56)
+       b[1] = byte(v >> 48)
+       b[2] = byte(v >> 40)
+       b[3] = byte(v >> 32)
+       b[4] = byte(v >> 24)
+       b[5] = byte(v >> 16)
+       b[6] = byte(v >> 8)
+       b[7] = byte(v)
+       return b
+}
index 8f4c3d98a5c4ed4676de19b7a4d33f96d38c6b52..8c5eccdde85f22126545cddf52439ff2c19b5a0c 100644 (file)
@@ -405,7 +405,9 @@ func (t *Fmp4Decoder) Search_stream_fmp4(buf []byte, keyframe *slice.Buf[byte])
        return
 }
 
-func (t *Fmp4Decoder) oneF(buf []byte, ifWrite func(t float64) bool, w ...io.Writer) (cu int, err error) {
+type dealF func(t float64, index int, buf *slice.Buf[byte]) error
+
+func (t *Fmp4Decoder) oneF(buf []byte, w ...dealF) (cu int, err error) {
        if len(buf) > humanize.MByte*100 {
                return 0, ErrBufTooLarge
        }
@@ -556,9 +558,7 @@ func (t *Fmp4Decoder) oneF(buf []byte, ifWrite func(t float64) bool, w ...io.Wri
                                        if v, e := t.buf.HadModified(bufModified); e == nil && v && !t.buf.IsEmpty() {
                                                cu = m[0].i
                                                if haveKeyframe && len(w) > 0 {
-                                                       if ifWrite(video.getT()) {
-                                                               _, err = w[0].Write(t.buf.GetPureBuf())
-                                                       }
+                                                       err = w[0](video.getT(), cu, t.buf)
                                                        t.buf.Reset()
                                                        return true, ErrNormal
                                                }
@@ -654,9 +654,7 @@ func (t *Fmp4Decoder) oneF(buf []byte, ifWrite func(t float64) bool, w ...io.Wri
                                        if v, e := t.buf.HadModified(bufModified); e == nil && v && !t.buf.IsEmpty() {
                                                cu = m[0].i
                                                if haveKeyframe && len(w) > 0 {
-                                                       if ifWrite(video.getT()) {
-                                                               _, err = w[0].Write(t.buf.GetPureBuf())
-                                                       }
+                                                       err = w[0](video.getT(), cu, t.buf)
                                                        t.buf.Reset()
                                                        return true, ErrNormal
                                                }
@@ -683,23 +681,32 @@ func (t *Fmp4Decoder) oneF(buf []byte, ifWrite func(t float64) bool, w ...io.Wri
        return
 }
 
+// Deprecated: 效率低于GenFastSeed+CutSeed
 func (t *Fmp4Decoder) Cut(reader io.Reader, startT, duration time.Duration, w io.Writer) (err error) {
+       return t.CutSeed(reader, startT, duration, w, nil, nil)
+}
+
+func (t *Fmp4Decoder) 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.MByte)
        buff := slice.New[byte]()
        init := false
+       seek := false
        over := false
        startTM := startT.Seconds()
        durationM := duration.Seconds()
        firstFT := -1.0
 
-       ifWriteF := func(t float64) bool {
+       wf := func(t float64, index int, buf *slice.Buf[byte]) (e error) {
                if firstFT == -1 {
                        firstFT = t
                }
                cu := t - firstFT
                over = duration != 0 && cu > durationM+startTM
-               return startTM <= cu && !over
+               if startTM <= cu && !over {
+                       _, e = w.Write(buf.GetPureBuf())
+               }
+               return
        }
 
        if t.Debug {
@@ -731,7 +738,70 @@ func (t *Fmp4Decoder) Cut(reader io.Reader, startT, duration time.Duration, w io
                                }
                        }
                } 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("w", e.Error())
+                       } else {
+                               if dropOffset != 0 {
+                                       _ = buff.RemoveFront(dropOffset)
+                               } else {
+                                       bufSize *= 2
+                               }
+                       }
+               }
+       }
+       return
+}
+
+func (t *Fmp4Decoder) GenFastSeed(reader io.Reader, save func(seedTo time.Duration, cuIndex int64) error) (err error) {
+       bufSize := humanize.KByte * 1100
+       totalRead := 0
+       buf := make([]byte, humanize.MByte)
+       buff := slice.New[byte]()
+       init := false
+       over := false
+       firstFT := -1.0
+
+       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 !init {
+                       if frontBuf, e := t.Init_fmp4(buff.GetPureBuf()); e != nil {
+                               return perrors.New("Init_fmp4", e.Error())
+                       } else {
+                               if len(frontBuf) == 0 {
+                                       bufSize *= 2
+                                       continue
+                               } else {
+                                       init = true
+                               }
+                       }
+               } else {
+                       if dropOffset, e := t.oneF(buff.GetPureBuf(), func(t float64, index int, buf *slice.Buf[byte]) error {
+                               if firstFT == -1 {
+                                       firstFT = t
+                               }
+                               return save(time.Second*time.Duration(t-firstFT), int64(totalRead-buff.Size()+index))
+                       }); e != nil {
                                return perrors.New("w", e.Error())
                        } else {
                                if dropOffset != 0 {
index 32454d2607a2ed67e3b758fd7f722d312251560c..367e437288be5b9ee142e3bb78b3f9a1cee4f6de 100644 (file)
@@ -8,6 +8,7 @@ import (
        "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"
@@ -63,20 +64,117 @@ func Test_deal(t *testing.T) {
        t.Log("max", humanize.Bytes(uint64(max)))
 }
 
+// 10s-30s 110.962896ms
+// 10m-10m20s 1.955749395s
+// 30m-30m20s 5.791614855s
 func Test_Mp4Cut(t *testing.T) {
+       {
+               st := time.Now()
+               defer func() {
+                       fmt.Println(time.Since(st))
+               }()
+       }
 
-       cutf := file.New("testdata/1.cut.mp4", 0, false)
+       cutf := file.New("testdata/0.cut.mp4", 0, false)
        defer cutf.Close()
        _ = cutf.Delete()
 
-       f := file.New("testdata/1.mp4", 0, false)
+       f := file.New("testdata/0.mp4", 0, false)
        defer f.Close()
 
        if f.IsDir() || !f.IsExist() {
                t.Log("test file not exist")
        }
 
-       e := NewFmp4Decoder().Cut(f, time.Second*10, time.Second*20, cutf.File())
+       e := NewFmp4Decoder().Cut(f, time.Minute*30, time.Second*20, cutf.File())
+       if perrors.Catch(e, "Read") {
+               t.Log("err Read", e)
+       }
+       if perrors.Catch(e, "Init_fmp4") {
+               t.Log("err Init_fmp4", 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_Mp4GenFastSeed(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.mp4", 0, false)
+       defer f.Close()
+       sf, e := VideoFastSeed.InitSav("testdata/0.fastSeed")
+       if e != nil {
+               t.Fatal(e)
+       }
+
+       e = NewFmp4Decoder().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, "Init_fmp4") {
+               t.Log("err Init_fmp4", e)
+       }
+       if perrors.Catch(e, "skip") {
+               t.Log("err skip", e)
+       }
+       if perrors.Catch(e, "cutW") {
+               t.Log("err cutW", e)
+       }
+       t.Log(e)
+
+       // VideoFastSeed.BeforeGet("testdata/1.fastSeed")
+       // {
+       //      index, _ := VideoFastSeed.GetIndex(0)
+       //      f.SeekIndex(index, file.AtOrigin)
+       //      buf := make([]byte, 8)
+       //      f.Read(buf)
+       //      fmt.Println(string(buf[4:8]))
+       // }
+}
+
+// 10s-30s 90.05983ms
+// 10m-10m20s 88.769475ms
+// 30m-30m20s 104.381225ms
+func Test_Mp4CutSeed(t *testing.T) {
+       {
+               st := time.Now()
+               defer func() {
+                       fmt.Println(time.Since(st))
+               }()
+       }
+
+       cutf := file.New("testdata/0.cut.mp4", 0, false)
+       defer cutf.Close()
+       _ = cutf.Delete()
+
+       f := file.New("testdata/0.mp4", 0, false)
+       defer f.Close()
+
+       if f.IsDir() || !f.IsExist() {
+               t.Log("test file not exist")
+       }
+
+       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.fastSeed")
+       if e != nil {
+               t.Fatal(e)
+       }
+
+       e = NewFmp4Decoder().CutSeed(f, time.Minute*30, time.Second*20, cutf.File(), f, gf)
        if perrors.Catch(e, "Read") {
                t.Log("err Read", e)
        }
index 5ab5eb74f9bfbae46720e4abb5658baa8b40372b..802239c1c3f4bf261227628680eeb8ee7a1dca0b 100644 (file)
@@ -1447,16 +1447,36 @@ func (t *M4SStream) Start() bool {
                                        go StartRecDanmu(contextC, ms.GetSavePath())
 
                                        //ass
-                                       if enc, ok := c.C.K_v.LoadV("Ass编码").(string); c.C.IsOn(`生成Ass弹幕`) && c.C.IsOn(`仅保存当前直播间流`) && ok {
+                                       if enc, ok := ms.common.K_v.LoadV("Ass编码").(string); ms.common.IsOn(`生成Ass弹幕`) && ms.common.IsOn(`仅保存当前直播间流`) && ok {
                                                go replyFunc.Ass.Ass_f(contextC, enc, ms.GetSavePath(), time.Now())
                                        }
 
+                                       path := ms.GetSavePath() + `0.` + ms.GetStreamType()
                                        startT := time.Now()
-                                       if e := ms.PusherToFile(contextC, ms.GetSavePath()+`0.`+ms.GetStreamType(), startf, stopf); e != nil {
+                                       if e := ms.PusherToFile(contextC, path, startf, stopf); e != nil {
                                                l.L(`E: `, e)
                                        }
                                        duration := time.Since(startT)
 
+                                       //PusherToFile fin genFastSeed
+                                       {
+                                               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
+                                                       }
+                                                       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) {
+                                                               l.L(`E: `, e)
+                                                       }
+                                                       f.Close()
+                                               default:
+                                               }
+                                       }
+
                                        // 结束,不发送空值停止直播回放
                                        // t.Stream_msg.PushLock_tag(`data`, []byte{})