From: qydysky Date: Mon, 11 Nov 2024 16:39:23 +0000 (+0800) Subject: Add 获取视频切片 X-Git-Tag: v0.14.22~27 X-Git-Url: http://127.0.0.1:8081/?a=commitdiff_plain;h=82079ccd1e8b56a9c01f71f7caa36ffc96be9a5c;p=bili_danmu%2F.git Add 获取视频切片 --- diff --git a/CV/Var.go b/CV/Var.go index 2528d3d..ffa9fa3 100644 --- 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 { diff --git a/README.md b/README.md index 2b329be..a711588 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,57 @@ ### 说明 本项目使用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 " 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)。默认为空,当不为空时,将在所有响应中添加指定头。 例子: diff --git a/Reply/F.go b/Reply/F.go index c69eb6d..e2e2634 100644 --- a/Reply/F.go +++ b/Reply/F.go @@ -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, diff --git a/Reply/F_test.go b/Reply/F_test.go index 086dc67..e6320be 100644 --- a/Reply/F_test.go +++ b/Reply/F_test.go @@ -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 diff --git a/Reply/flvDecode.go b/Reply/flvDecode.go index 8b5f782..5c1092e 100644 --- a/Reply/flvDecode.go +++ b/Reply/flvDecode.go @@ -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) diff --git a/Reply/flvDecode_test.go b/Reply/flvDecode_test.go index 854eb5f..076678c 100644 --- a/Reply/flvDecode_test.go +++ b/Reply/flvDecode_test.go @@ -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) +} diff --git a/Reply/fmp4Decode.go b/Reply/fmp4Decode.go index 1f25d28..3de55e8 100644 --- a/Reply/fmp4Decode.go +++ b/Reply/fmp4Decode.go @@ -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}) } diff --git a/Reply/fmp4Decode_test.go b/Reply/fmp4Decode_test.go index 6e4f0b9..a544983 100644 --- a/Reply/fmp4Decode_test.go +++ b/Reply/fmp4Decode_test.go @@ -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) +} diff --git a/demo/config/config_K_v.json b/demo/config/config_K_v.json index ff121c2..ebc0bc2 100644 --- a/demo/config/config_K_v.json +++ b/demo/config/config_K_v.json @@ -162,6 +162,8 @@ "直播流回放速率": "2 MB", "直播流回放连接检查-help": "默认-1,小于1禁用。指最多支持n连接流,有效数时,会对回放连接进行定时检查。原因,对于经过代理回放,有可能浏览器标签页已经关闭,但代理不关闭连接,导致连接不能释放", "直播流回放连接检查": -1, + "直播流回放连接检查忽略key-help": "字符串数组,默认空,空字符串将忽略,当不为空时,将不会定时检查指定key值的请求", + "直播流回放连接检查忽略key": [""], "直播流回放连接限制-help": "限制回放连接数,<0无限制,=0禁止,>0最大数量", "直播流回放连接限制": [ {