* 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)。默认为空,当不为空时,将在所有响应中添加指定头。
例子:
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 {
_ "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"
)
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
--- /dev/null
+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
+}
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
}
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
}
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
}
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 {
}
}
} 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 {
"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 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)
}
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{})