]> 127.0.0.1 Git - bili_danmu/.git/commitdiff
Add 获取视频切片
authorqydysky <qydysky@foxmail.com>
Mon, 11 Nov 2024 16:39:23 +0000 (00:39 +0800)
committerqydysky <qydysky@foxmail.com>
Mon, 11 Nov 2024 16:39:23 +0000 (00:39 +0800)
CV/Var.go
README.md
Reply/F.go
Reply/F_test.go
Reply/flvDecode.go
Reply/flvDecode_test.go
Reply/fmp4Decode.go
Reply/fmp4Decode_test.go
demo/config/config_K_v.json

index 2528d3d2cdc6049734339bebacad9419cedea093..ffa9fa33442325054d227d4350cbab8ca9fc061a 100644 (file)
--- a/CV/Var.go
+++ b/CV/Var.go
@@ -629,7 +629,7 @@ func (t *Common) Init() *Common {
                                if createok {
                                        tx := psql.BeginTx[any](db, pctx.GenTOCtx(time.Second*5))
                                        tx.Do(psql.SqlFunc[any]{
-                                               Query:      create,
+                                               Sql:        create,
                                                SkipSqlErr: true,
                                        })
                                        if _, e := tx.Fin(); e != nil {
index 2b329be3f649b8f234c3c89aabc0a8dd0265b252..a71158833257b2343e0602032fb7a45f34465dbf 100644 (file)
--- a/README.md
+++ b/README.md
 ### 说明
 本项目使用github action自动构建,构建过程详见[yml](https://github.com/qydysky/bili_danmu/blob/master/.github/workflows/go.yml)
 
+#### 从录播文件获取指定时间、指定时长的切片视频
+当请求`http://{Web服务地址}{直播Web服务路径}stream/ref={录播文件夹名}&st={起始时间}&dur={片段时长}`时,将返回从录播文件的切片视频(>v0.14.21)
+
+切片将从大于`{起始时间}`的关键帧开始,`{片段时长}`之后的关键帧结束,故大多数情况不能获得精确时间的切片视频
+
+其中`dur`为空时,将返回全部时长。`st`参数可以为空或不传,此时从录播文件起始点开始。
+
+`{起始时间}`、`{片段时长}`格式使用[time.ParseDuration](https://pkg.go.dev/time#ParseDuration)进行转换。例:`1m`为1分钟、`1h2m`为1小时2分。
+
+注意:当配置`直播流回放连接检查`启用时(默认不启用),你需要配置`直播流回放连接检查忽略key`(>v0.14.21)以避免检查,url加上参数`&key={配置的key}`。
+
+例子:
+
+```json
+{
+  "Web服务地址":"0.0.0.0:11000",
+  "直播Web服务路径":"/web/",
+  "直播流回放连接检查": 10,
+  "直播流回放连接检查忽略key-help": "字符串数组,默认空,空字符串将忽略,当不为空时,将不会定时检查指定key值的请求",
+  "直播流回放连接检查忽略key": ["cut"],
+}
+```
+
+```
+curl -v "http://192.168.31.230:20000/web/stream?ref=2024_11_04-01_29_47-47867-250-edd590-JdB&key=cut&dur=1m"
+*   Trying 192.168.31.230:20000...
+* Connected to 192.168.31.230 (192.168.31.230) port 20000
+> GET /web/stream?ref=2024_11_04-01_29_47-47867-250-edd590-JdB&key=cut&dur=1m HTTP/1.1
+> Host: 192.168.31.230:20000
+> User-Agent: curl/8.9.1
+> Accept: */*
+> 
+* Request completely sent off
+< HTTP/1.1 200 OK
+< Access-Control-Allow-Credentials: true
+< Access-Control-Allow-Headers: *
+< Access-Control-Allow-Methods: POST, GET, OPTIONS
+< Access-Control-Allow-Origin: *
+< Connection: keep-alive
+< Content-Disposition: inline; filename="2024_11_04-01_29_47-47867-250-edd590-JdB.1731342591.mp4"
+< Content-Transfer-Encoding: binary
+< Content-Type: flv-application/octet-stream
+< Date: Mon, 11 Nov 2024 16:29:51 GMT
+< Transfer-Encoding: chunked
+< 
+Warning: Binary output can mess up your terminal. Use "--output -" to tell curl to output it to your terminal anyway, or consider "--output <FILE>" to save to a file.
+* client returned ERROR on write of 1004 bytes
+* Failed reading the chunked-encoded stream
+* closing connection #0
+```
+
 #### Web自定义响应头
 配置文件中添加配置项`Web自定义响应头`(>v0.14.19)。默认为空,当不为空时,将在所有响应中添加指定头。
 例子:
index c69eb6d8e462f41fdc0c26feb3c068486e5a4fe7..e2e263474e21daed940739bc817f6623aba78ef4 100644 (file)
@@ -1370,11 +1370,27 @@ func init() {
                                return
                        }
 
-                       if e := expirer.LoopCheck(r.Context(), r.URL.Query().Get("key"), func(key string, e error) {
-                               _ = c.C.SerF.GetConn(r).Close()
-                       }); e != nil {
-                               w.WriteHeader(http.StatusTooManyRequests)
-                               return
+                       // 检查key
+                       {
+                               var checkKey = true
+
+                               if v, ok := c.C.K_v.LoadV(`直播流回放连接检查忽略key`).([]any); ok && len(v) != 0 {
+                                       for i := 0; i < len(v); i++ {
+                                               if s, ok := v[i].(string); ok && s != "" && r.URL.Query().Get("key") == s {
+                                                       checkKey = false
+                                                       break
+                                               }
+                                       }
+                               }
+
+                               if checkKey {
+                                       if e := expirer.LoopCheck(r.Context(), r.URL.Query().Get("key"), func(key string, e error) {
+                                               _ = c.C.SerF.GetConn(r).Close()
+                                       }); e != nil {
+                                               w.WriteHeader(http.StatusTooManyRequests)
+                                               return
+                                       }
+                               }
                        }
 
                        //header
@@ -1385,9 +1401,14 @@ func init() {
                        w.Header().Set("Connection", "keep-alive")
                        w.Header().Set("Content-Transfer-Encoding", "binary")
 
-                       var rpath string
+                       var (
+                               rpath       string
+                               qref        = r.URL.Query().Get("ref")
+                               startT, _   = time.ParseDuration(r.URL.Query().Get("st"))
+                               duration, _ = time.ParseDuration(r.URL.Query().Get("dur"))
+                       )
 
-                       if qref := r.URL.Query().Get("ref"); rpath == "" && qref != "" {
+                       if rpath == "" && qref != "" {
                                rpath = "/" + qref + "/"
                        }
 
@@ -1476,7 +1497,20 @@ func init() {
                                        ts := time.Now()
                                        defer func() { flog.L(`T: `, r.RemoteAddr, `断开录播`, time.Since(ts)) }()
 
-                                       if e := f.CopyToIoWriter(w, pio.CopyConfig{BytePerSec: speed}); e != nil {
+                                       if duration != 0 {
+                                               if strings.HasSuffix(v, "flv") {
+                                                       w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s.%d.mp4\"", qref, time.Now().Unix()))
+                                                       if e := NewFlvDecoder().Cut(f, startT, duration, w); e != nil && !errors.Is(e, io.EOF) {
+                                                               flog.L(`E: `, e)
+                                                       }
+                                               }
+                                               if strings.HasSuffix(v, "mp4") {
+                                                       w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s.%d.flv\"", qref, time.Now().Unix()))
+                                                       if e := NewFmp4Decoder().Cut(f, startT, duration, w); e != nil && !errors.Is(e, io.EOF) {
+                                                               flog.L(`E: `, e)
+                                                       }
+                                               }
+                                       } else if e := f.CopyToIoWriter(w, pio.CopyConfig{BytePerSec: speed}); e != nil {
                                                flog.L(`E: `, e)
                                        }
                                        // }
@@ -1778,7 +1812,7 @@ func (t *SaveDanmuToDB) init(c *c.Common) {
                                t.db = db
                                if createok {
                                        tx := psql.BeginTx[any](db, pctx.GenTOCtx(time.Second*5))
-                                       tx.Do(psql.SqlFunc[any]{Query: create, SkipSqlErr: true})
+                                       tx.Do(psql.SqlFunc[any]{Sql: create, SkipSqlErr: true})
                                        if _, e := tx.Fin(); e != nil {
                                                c.Log.Base_add("保存弹幕至db").L(`E: `, e)
                                                return
@@ -1813,7 +1847,7 @@ func (t *SaveDanmuToDB) danmu(item Danmu_item) {
                }
 
                tx := psql.BeginTx[any](t.db, pctx.GenTOCtx(time.Second*5))
-               tx.DoPlaceHolder(psql.SqlFunc[any]{Query: t.insert}, &DanmuI{
+               tx.DoPlaceHolder(psql.SqlFunc[any]{Sql: t.insert}, &DanmuI{
                        Date:   time.Now().Format(time.DateTime),
                        Unix:   time.Now().Unix(),
                        Msg:    item.msg,
index 086dc6745cc2cd8fe579f68ccbbc0263f0df3e08..e6320be55b3920421a25ad6cd7ee293cc5633090 100644 (file)
@@ -34,7 +34,7 @@ func TestSaveDanmuToDB(t *testing.T) {
                t.Fatal(e)
        } else {
                tx := psql.BeginTx[any](db, pctx.GenTOCtx(time.Second*5))
-               tx.Do(psql.SqlFunc[any]{Query: "select msg as Msg from danmu"})
+               tx.Do(psql.SqlFunc[any]{Sql: "select msg as Msg from danmu"})
                tx.AfterQF(func(_ *any, rows *sql.Rows, e *error) {
                        type row struct {
                                Msg string
index 8b5f7829baa71ddf2b08243b3060f259a353cba0..5c1092e7ea3fa436cf0cb55d1c1cf4c51ecd829b 100644 (file)
@@ -3,9 +3,13 @@ package reply
 import (
        "errors"
        "fmt"
+       "io"
        "math"
+       "time"
 
+       "github.com/dustin/go-humanize"
        F "github.com/qydysky/bili_danmu/F"
+       perrors "github.com/qydysky/part/errors"
        slice "github.com/qydysky/part/slice"
 )
 
@@ -197,6 +201,138 @@ func (t *FlvDecoder) SearchStreamTag(buf []byte, keyframe *slice.Buf[byte]) (dro
        return
 }
 
+func (t *FlvDecoder) oneF(buf []byte, w ...io.Writer) (dropT int, dropOffset int, err error) {
+
+       if !t.init {
+               err = ErrNoInit
+               return
+       }
+
+       var (
+               keyframeOp = -1
+               lastAT     int
+               lastVT     int
+       )
+
+       for bufOffset := 0; bufOffset >= 0 && bufOffset+tagHeaderSize < len(buf); {
+
+               if buf[bufOffset]&0b11000000 != 0 &&
+                       buf[bufOffset]&0b00011111 != videoTag &&
+                       buf[bufOffset]&0b00011111 != audioTag &&
+                       buf[bufOffset]&0b00011111 != scriptTag {
+                       err = ErrNoFoundTagHeader
+                       return
+               }
+
+               if buf[bufOffset+8]|buf[bufOffset+9]|buf[bufOffset+10] != streamId {
+                       err = ErrStreamId
+                       return
+               }
+
+               tagSize := int(F.Btoi32([]byte{0x00, buf[bufOffset+1], buf[bufOffset+2], buf[bufOffset+3]}, 0))
+               if tagSize == 0 {
+                       err = ErrTagSizeZero
+                       return
+               }
+               if bufOffset+tagHeaderSize+tagSize+previouTagSize > len(buf) {
+                       return
+               }
+
+               tagSizeCheck := int(F.Btoi32(buf[bufOffset+tagHeaderSize+tagSize:bufOffset+tagHeaderSize+tagSize+previouTagSize], 0))
+               if tagSizeCheck != tagSize+tagHeaderSize {
+                       err = ErrTagSize
+                       return
+               }
+
+               timeStamp := int(F.Btoi32([]byte{buf[bufOffset+7], buf[bufOffset+4], buf[bufOffset+5], buf[bufOffset+6]}, 0))
+               switch {
+               case buf[bufOffset] == videoTag:
+                       lastVT = timeStamp
+               case buf[bufOffset] == audioTag:
+                       lastAT = timeStamp
+               default:
+               }
+               if lastAT != 0 && lastVT != 0 {
+                       diff := math.Abs(float64(lastVT - lastAT))
+                       if diff > t.Diff {
+                               err = fmt.Errorf("时间戳不匹配 %v %v (或许应调整flv音视频时间戳容差ms>%f)", lastVT, lastAT, diff)
+                               return
+                       }
+               }
+
+               if buf[bufOffset] == videoTag && buf[bufOffset+11]&0xf0 == 0x10 { //key frame
+                       if keyframeOp >= 0 {
+                               dropOffset = bufOffset
+                               if len(w) > 0 {
+                                       w[0].Write(buf[keyframeOp:bufOffset])
+                               }
+                               return
+                       }
+                       dropT = timeStamp
+                       keyframeOp = bufOffset
+               }
+               bufOffset += tagSizeCheck + previouTagSize
+       }
+
+       return
+}
+
+func (t *FlvDecoder) Cut(reader io.Reader, startT, duration time.Duration, w io.Writer) (err error) {
+       buf := make([]byte, humanize.MByte)
+       buff := slice.New[byte](10 * humanize.MByte)
+       skiped := false
+       startTM := startT.Milliseconds()
+       durationM := duration.Milliseconds()
+       firstFT := -1
+       for c := 0; err == nil; c++ {
+               n, e := reader.Read(buf)
+               if n == 0 && errors.Is(e, io.EOF) {
+                       return io.EOF
+               }
+               err = buff.Append(buf[:n])
+
+               if !t.init {
+                       if frontBuf, dropOffset, e := t.InitFlv(buf); e != nil {
+                               return perrors.New("InitFlv", e.Error())
+                       } else {
+                               if dropOffset != 0 {
+                                       _ = buff.RemoveFront(dropOffset)
+                               }
+                               if len(frontBuf) == 0 {
+                                       continue
+                               } else {
+                                       w.Write(frontBuf)
+                               }
+                       }
+               } else if !skiped {
+                       if dropT, dropOffset, e := t.oneF(buff.GetPureBuf()); e != nil {
+                               return perrors.New("skip", e.Error())
+                       } else {
+                               if dropOffset != 0 {
+                                       _ = buff.RemoveFront(dropOffset)
+                               }
+                               if firstFT == -1 {
+                                       firstFT = dropT
+                               } else if startTM < int64(dropT-firstFT) {
+                                       skiped = true
+                               }
+                       }
+               } else {
+                       if dropT, dropOffset, e := t.oneF(buff.GetPureBuf(), w); e != nil {
+                               return perrors.New("w", e.Error())
+                       } else {
+                               if dropOffset != 0 {
+                                       _ = buff.RemoveFront(dropOffset)
+                               }
+                               if durationM+startTM < int64(dropT-firstFT) {
+                                       return
+                               }
+                       }
+               }
+       }
+       return
+}
+
 func (t *FlvDecoder) Parse(buf []byte, keyframe *slice.Buf[byte]) (frontBuf []byte, dropOffset int, err error) {
        if !t.init {
                frontBuf, dropOffset, err = t.InitFlv(buf)
index 854eb5fa4609d85211ebb463a41da9f8572e792a..076678c35fa86a21dda3bc738cb3c70fafa1fc00 100644 (file)
@@ -5,8 +5,10 @@ import (
        "fmt"
        "io"
        "testing"
+       "time"
 
        "github.com/dustin/go-humanize"
+       perrors "github.com/qydysky/part/errors"
        file "github.com/qydysky/part/file"
        slice "github.com/qydysky/part/slice"
 )
@@ -48,3 +50,32 @@ func Test_FLVdeal(t *testing.T) {
        }
        t.Log("max", humanize.Bytes(uint64(max)))
 }
+
+func _Test_FLVCut(t *testing.T) {
+
+       cutf := file.New("testdata/1.cut.flv", 0, false)
+       defer cutf.Close()
+       cutf.Delete()
+
+       f := file.New("testdata/1.flv", 0, false)
+       defer f.Close()
+
+       if f.IsDir() || !f.IsExist() {
+               t.Fatal("file not support")
+       }
+
+       e := NewFlvDecoder().Cut(f, time.Second*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)
+}
index 1f25d285158e97ea69adffd4eafd421f0becc4db..3de55e8282685b5a086d74073cb098a25d1ef13b 100644 (file)
@@ -6,6 +6,7 @@ import (
        "fmt"
        "io"
        "math"
+       "time"
 
        "github.com/dustin/go-humanize"
        F "github.com/qydysky/bili_danmu/F"
@@ -403,6 +404,337 @@ func (t *Fmp4Decoder) Search_stream_fmp4(buf []byte, keyframe *slice.Buf[byte])
        return
 }
 
+func (t *Fmp4Decoder) oneF(buf []byte, w ...io.Writer) (cuT float64, cu int, err error) {
+       if len(buf) > humanize.MByte*100 {
+               return 0, 0, ErrBufTooLarge
+       }
+       if len(t.traks) == 0 {
+               return 0, 0, ErrMisTraks
+       }
+       t.buf.Reset()
+
+       defer func() {
+               if err != nil {
+                       cu = 0
+               }
+       }()
+
+       var (
+               haveKeyframe bool
+               bufModified  = t.buf.GetModified()
+               // maxSequenceNumber int //有时并不是单调增加
+               maxVT float64
+               maxAT float64
+
+               //get timeStamp
+               get_timeStamp = func(tfdt int) (ts timeStamp) {
+                       switch buf[tfdt+8] {
+                       case 0:
+                               ts.data = buf[tfdt+16 : tfdt+20]
+                               ts.timeStamp = int(F.Btoi32(buf, tfdt+16))
+                       case 1:
+                               ts.data = buf[tfdt+12 : tfdt+20]
+                               ts.timeStamp = int(F.Btoi64(buf, tfdt+12))
+                       }
+                       return
+               }
+
+               //get track type
+               get_track_type = func(tfhd, tfdt int) (ts timeStamp, handlerType byte) {
+                       track, ok := t.traks[int(F.Btoi(buf, tfhd+12, 4))]
+                       if ok {
+                               ts := get_timeStamp(tfdt)
+                               // if track.firstTimeStamp == -1 {
+                               //      track.firstTimeStamp = ts.timeStamp
+                               // }
+
+                               // ts.firstTimeStamp = track.firstTimeStamp
+                               ts.handlerType = track.handlerType
+                               ts.timescale = track.timescale
+
+                               // if ts.timeStamp > track.lastTimeStamp {
+                               //      track.lastTimeStamp = ts.timeStamp
+                               //      ts.resetTs()
+                               // }
+
+                               return ts, track.handlerType
+                       }
+                       return
+               }
+
+               //is SampleEntries error?
+               checkSampleEntries = func(trun, mdat int) error {
+                       if buf[trun+11] == 'b' {
+                               for i := trun + 24; i < mdat; i += 12 {
+                                       if F.Btoi(buf, i+4, 4) < 1000 {
+                                               return errors.New("find sample size less then 1000")
+                                       }
+                               }
+                       }
+                       return nil
+               }
+
+               //is t error?
+               check_set_maxT = func(ts timeStamp, equal func(ts timeStamp) error, larger func(ts timeStamp) error) (err error) {
+                       switch ts.handlerType {
+                       case 'v':
+                               if maxVT == 0 {
+                                       maxVT = ts.getT()
+                               } else if maxVT == ts.getT() && equal != nil {
+                                       err = equal(ts)
+                               } else if maxVT > ts.getT() && larger != nil {
+                                       err = larger(ts)
+                               } else {
+                                       maxVT = ts.getT()
+                               }
+                       case 'a':
+                               if maxAT == 0 {
+                                       maxAT = ts.getT()
+                               } else if maxAT == ts.getT() && equal != nil {
+                                       err = equal(ts)
+                               } else if maxAT > ts.getT() && larger != nil {
+                                       err = larger(ts)
+                               } else {
+                                       maxAT = ts.getT()
+                               }
+                       default:
+                       }
+                       return
+               }
+       )
+
+       ies, e := decode(buf, "moof")
+       if e != nil {
+               return 0, 0, e
+       }
+
+       var ErrNormal = perrors.New("ErrNormal", "ErrNormal")
+
+       err = deals(ies,
+               [][]string{
+                       {"moof", "mfhd", "traf", "tfhd", "tfdt", "trun", "mdat"},
+                       {"moof", "mfhd", "traf", "tfhd", "tfdt", "trun", "traf", "tfhd", "tfdt", "trun", "mdat"}},
+               []func(m []ie) (bool, error){
+                       func(m []ie) (bool, error) {
+                               var (
+                                       keyframeMoof = buf[m[5].i+20] == byte(0x02)
+                                       // moofSN       = int(F.Btoi(buf, m[1].i+12, 4))
+                                       video timeStamp
+                               )
+
+                               {
+                                       ts, handlerType := get_track_type(m[3].i, m[4].i)
+                                       if ts.handlerType == 'v' {
+                                               if e := checkSampleEntries(m[5].i, m[6].i); e != nil {
+                                                       //skip
+                                                       t.buf.Reset()
+                                                       haveKeyframe = false
+                                                       cu = m[0].i
+                                                       return false, e
+                                               }
+                                       }
+                                       if handlerType == 'v' {
+                                               video = ts
+                                       }
+                                       if e := check_set_maxT(ts, func(_ timeStamp) error {
+                                               return errors.New("skip")
+                                       }, func(_ timeStamp) error {
+                                               t.buf.Reset()
+                                               haveKeyframe = false
+                                               cu = m[0].i
+                                               return errors.New("skip")
+                                       }); e != nil {
+                                               return false, e
+                                       }
+                               }
+
+                               // fmt.Println(ts.getT(), "frame0", keyframeMoof, t.buf.size(), m[0].i, m[6].n, m[6].e)
+
+                               //deal frame
+                               if keyframeMoof {
+                                       if v, e := t.buf.HadModified(bufModified); e == nil && v && !t.buf.IsEmpty() {
+                                               cu = m[0].i
+                                               cuT = video.getT()
+                                               if haveKeyframe && len(w) > 0 {
+                                                       w[0].Write(t.buf.GetPureBuf())
+                                                       return true, ErrNormal
+                                               }
+                                               t.buf.Reset()
+                                       }
+                                       haveKeyframe = true
+                               } else if !haveKeyframe {
+                                       cu = m[6].e
+                               }
+                               if haveKeyframe {
+                                       if e := t.buf.Append(buf[m[0].i:m[6].e]); e != nil {
+                                               return false, e
+                                       }
+                               }
+                               return false, nil
+                       },
+                       func(m []ie) (bool, error) {
+                               var (
+                                       keyframeMoof = buf[m[5].i+20] == byte(0x02) || buf[m[9].i+20] == byte(0x02)
+                                       // moofSN       = int(F.Btoi(buf, m[1].i+12, 4))
+                                       video timeStamp
+                                       audio timeStamp
+                               )
+
+                               // fmt.Println(moofSN, "frame1", keyframeMoof, t.buf.size(), m[0].i, m[10].n, m[10].e)
+
+                               {
+                                       ts, handlerType := get_track_type(m[3].i, m[4].i)
+                                       if handlerType == 'v' {
+                                               if e := checkSampleEntries(m[5].i, m[6].i); e != nil {
+                                                       //skip
+                                                       t.buf.Reset()
+                                                       haveKeyframe = false
+                                                       cu = m[0].i
+                                                       return false, e
+                                               }
+                                       }
+                                       switch handlerType {
+                                       case 'v':
+                                               video = ts
+                                       case 's':
+                                               audio = ts
+                                       }
+                                       if e := check_set_maxT(ts, func(_ timeStamp) error {
+                                               return errors.New("skip")
+                                       }, func(_ timeStamp) error {
+                                               t.buf.Reset()
+                                               haveKeyframe = false
+                                               cu = m[0].i
+                                               return errors.New("skip")
+                                       }); e != nil {
+                                               return false, e
+                                       }
+                               }
+                               {
+                                       ts, handlerType := get_track_type(m[7].i, m[8].i)
+                                       if handlerType == 'v' {
+                                               if e := checkSampleEntries(m[9].i, m[10].i); e != nil {
+                                                       //skip
+                                                       t.buf.Reset()
+                                                       haveKeyframe = false
+                                                       cu = m[0].i
+                                                       return false, e
+                                               }
+                                       }
+                                       switch handlerType {
+                                       case 'v':
+                                               video = ts
+                                       case 's':
+                                               audio = ts
+                                       }
+                                       if e := check_set_maxT(ts, func(_ timeStamp) error {
+                                               return errors.New("skip")
+                                       }, func(_ timeStamp) error {
+                                               t.buf.Reset()
+                                               haveKeyframe = false
+                                               cu = m[0].i
+                                               return errors.New("skip")
+                                       }); e != nil {
+                                               return false, e
+                                       }
+                               }
+
+                               //sync audio timeStamp
+                               if t.AVTDiff <= 0.1 {
+                                       t.AVTDiff = 0.1
+                               }
+                               if diff := math.Abs(video.getT() - audio.getT()); diff > t.AVTDiff {
+                                       return false, fmt.Errorf("时间戳不匹配 %v %v (或许应调整fmp4音视频时间戳容差s>%.2f)", video.timeStamp, audio.timeStamp, diff)
+                                       // copy(video.data, F.Itob64(int64(audio.getT()*float64(video.timescale))))
+                               }
+
+                               //deal frame
+                               if keyframeMoof {
+                                       if v, e := t.buf.HadModified(bufModified); e == nil && v && !t.buf.IsEmpty() {
+                                               cu = m[0].i
+                                               cuT = video.getT()
+                                               if haveKeyframe && len(w) > 0 {
+                                                       w[0].Write(t.buf.GetPureBuf())
+                                                       return true, ErrNormal
+                                               }
+                                               t.buf.Reset()
+                                       }
+                                       haveKeyframe = true
+                               } else if !haveKeyframe {
+                                       cu = m[10].e
+                               }
+                               if haveKeyframe {
+                                       if e := t.buf.Append(buf[m[0].i:m[10].e]); e != nil {
+                                               return false, e
+                                       }
+                               }
+                               return false, nil
+                       },
+               },
+       )
+
+       if errors.Is(err, ErrNormal) {
+               err = nil
+       }
+
+       return
+}
+
+func (t *Fmp4Decoder) Cut(reader io.Reader, startT, duration time.Duration, w io.Writer) (err error) {
+       buf := make([]byte, humanize.MByte)
+       buff := slice.New[byte](10 * humanize.MByte)
+       init := false
+       skiped := false
+       startTM := startT.Seconds()
+       durationM := duration.Seconds()
+       firstFT := -1.0
+       for c := 0; err == nil; c++ {
+               n, e := reader.Read(buf)
+               if n == 0 && errors.Is(e, io.EOF) {
+                       return io.EOF
+               }
+               err = buff.Append(buf[:n])
+
+               if !init {
+                       if frontBuf, e := t.Init_fmp4(buf); e != nil {
+                               return perrors.New("Init_fmp4", e.Error())
+                       } else {
+                               if len(frontBuf) == 0 {
+                                       continue
+                               } else {
+                                       init = true
+                                       w.Write(frontBuf)
+                               }
+                       }
+               } else if !skiped {
+                       if dropT, dropOffset, e := t.oneF(buff.GetPureBuf()); e != nil {
+                               return perrors.New("skip", e.Error())
+                       } else {
+                               if dropOffset != 0 {
+                                       _ = buff.RemoveFront(dropOffset)
+                               }
+                               if firstFT == -1 {
+                                       firstFT = dropT
+                               } else if startTM < dropT-firstFT {
+                                       skiped = true
+                               }
+                       }
+               } else {
+                       if dropT, dropOffset, e := t.oneF(buff.GetPureBuf(), w); e != nil {
+                               return perrors.New("w", e.Error())
+                       } else {
+                               if dropOffset != 0 {
+                                       _ = buff.RemoveFront(dropOffset)
+                               }
+                               if durationM+startTM < dropT-firstFT {
+                                       return
+                               }
+                       }
+               }
+       }
+       return
+}
+
 func deal(ies []ie, boxNames []string, fs func([]ie) (breakloop bool, err error)) (err error) {
        return deals(ies, [][]string{boxNames}, []func([]ie) (breakloop bool, err error){fs})
 }
index 6e4f0b9afa756ffc52c38f543abe597c996e9116..a5449831d0fe01b6f7977756579933bf87e19f09 100644 (file)
@@ -5,8 +5,10 @@ import (
        "fmt"
        "io"
        "testing"
+       "time"
 
        "github.com/dustin/go-humanize"
+       perrors "github.com/qydysky/part/errors"
        file "github.com/qydysky/part/file"
        slice "github.com/qydysky/part/slice"
 )
@@ -60,3 +62,32 @@ func Test_deal(t *testing.T) {
        }
        t.Log("max", humanize.Bytes(uint64(max)))
 }
+
+func _Test_Mp4Cut(t *testing.T) {
+
+       cutf := file.New("testdata/1.cut.mp4", 0, false)
+       defer cutf.Close()
+       cutf.Delete()
+
+       f := file.New("testdata/1.mp4", 0, false)
+       defer f.Close()
+
+       if f.IsDir() || !f.IsExist() {
+               t.Fatal("file not support")
+       }
+
+       e := NewFmp4Decoder().Cut(f, time.Second*10, 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)
+}
index ff121c2bc4342c56d3fbd673922b98bffc9d5b61..ebc0bc26d3f6c0463fccddead6e5f3008309c832 100644 (file)
     "直播流回放速率": "2 MB",
     "直播流回放连接检查-help": "默认-1,小于1禁用。指最多支持n连接流,有效数时,会对回放连接进行定时检查。原因,对于经过代理回放,有可能浏览器标签页已经关闭,但代理不关闭连接,导致连接不能释放",
     "直播流回放连接检查": -1,
+    "直播流回放连接检查忽略key-help": "字符串数组,默认空,空字符串将忽略,当不为空时,将不会定时检查指定key值的请求",
+    "直播流回放连接检查忽略key": [""],
     "直播流回放连接限制-help": "限制回放连接数,<0无限制,=0禁止,>0最大数量",
     "直播流回放连接限制": [
         {