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 {
### 说明
本项目使用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)。默认为空,当不为空时,将在所有响应中添加指定头。
例子:
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
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 + "/"
}
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)
}
// }
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
}
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,
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
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"
)
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)
"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"
)
}
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)
+}
"fmt"
"io"
"math"
+ "time"
"github.com/dustin/go-humanize"
F "github.com/qydysky/bili_danmu/F"
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})
}
"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"
)
}
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)
+}
"直播流回放速率": "2 MB",
"直播流回放连接检查-help": "默认-1,小于1禁用。指最多支持n连接流,有效数时,会对回放连接进行定时检查。原因,对于经过代理回放,有可能浏览器标签页已经关闭,但代理不关闭连接,导致连接不能释放",
"直播流回放连接检查": -1,
+ "直播流回放连接检查忽略key-help": "字符串数组,默认空,空字符串将忽略,当不为空时,将不会定时检查指定key值的请求",
+ "直播流回放连接检查忽略key": [""],
"直播流回放连接限制-help": "限制回放连接数,<0无限制,=0禁止,>0最大数量",
"直播流回放连接限制": [
{