From 7c2aa71268d34ba5ce1dc67cd196041b4c070ed6 Mon Sep 17 00:00:00 2001 From: qydysky Date: Mon, 13 Jan 2025 02:13:24 +0800 Subject: [PATCH] =?utf8?q?Add=20fmp4=E5=BF=AB=E9=80=9F=E7=B4=A2=E5=BC=95?= =?utf8?q?=E6=96=87=E4=BB=B6=20(#146)?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * Add fmp4快速索引文件 * Fix golangci-lint * Fix golangci-lint --- README.md | 29 ++++++ Reply/F.go | 13 ++- Reply/F/comp.go | 6 ++ Reply/F/videoFastSeed/videoFastSeed.go | 131 +++++++++++++++++++++++++ Reply/fmp4Decode.go | 90 +++++++++++++++-- Reply/fmp4Decode_test.go | 104 +++++++++++++++++++- Reply/stream.go | 24 ++++- 7 files changed, 380 insertions(+), 17 deletions(-) create mode 100644 Reply/F/videoFastSeed/videoFastSeed.go diff --git a/README.md b/README.md index 814f22e..dde3534 100644 --- 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)。默认为空,当不为空时,将在所有响应中添加指定头。 例子: diff --git a/Reply/F.go b/Reply/F.go index 0871382..d5547bd 100644 --- a/Reply/F.go +++ b/Reply/F.go @@ -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 { diff --git a/Reply/F/comp.go b/Reply/F/comp.go index 7add94a..3c4406e 100644 --- a/Reply/F/comp.go +++ b/Reply/F/comp.go @@ -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 index 0000000..3198844 --- /dev/null +++ b/Reply/F/videoFastSeed/videoFastSeed.go @@ -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 +} diff --git a/Reply/fmp4Decode.go b/Reply/fmp4Decode.go index 8f4c3d9..8c5eccd 100644 --- a/Reply/fmp4Decode.go +++ b/Reply/fmp4Decode.go @@ -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 { diff --git a/Reply/fmp4Decode_test.go b/Reply/fmp4Decode_test.go index 32454d2..367e437 100644 --- a/Reply/fmp4Decode_test.go +++ b/Reply/fmp4Decode_test.go @@ -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) } diff --git a/Reply/stream.go b/Reply/stream.go index 5ab5eb7..802239c 100644 --- a/Reply/stream.go +++ b/Reply/stream.go @@ -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{}) -- 2.39.2