// Copyright 2019, Chef. All rights reserved. // https://github.com/q191201771/lal // // Use of this source code is governed by a MIT-style license // that can be found in the License file. // // Author: Chef (191201771@qq.com) package main import ( "flag" "fmt" "io" "os" "strconv" "strings" "time" "github.com/q191201771/lal/pkg/logic" "github.com/q191201771/lal/pkg/httpflv" "github.com/q191201771/lal/pkg/rtmp" "github.com/q191201771/naza/pkg/bininfo" log "github.com/q191201771/naza/pkg/nazalog" ) // RTMP推流客户端,读取本地FLV文件,使用RTMP协议推送出去 // // 支持循环推送:文件推送完毕后,可循环推送(RTMP push 流并不断开) // 支持推送多路流:相当于一个RTMP推流压测工具 // // Usage of ./bin/flvfile2rtmppush: // -i string // specify flv file // -n int // num of push connection (default 1) // -o string // specify rtmp push url // -r recursive push if reach end of file // -v show bin info // Example: // ./bin/flvfile2rtmppush -i testdata/test.flv -o rtmp://127.0.0.1:19350/live/test // ./bin/flvfile2rtmppush -i testdata/test.flv -o rtmp://127.0.0.1:19350/live/test -r // ./bin/flvfile2rtmppush -i testdata/test.flv -o rtmp://127.0.0.1:19350/live/test_{i} -r -n 1000 func main() { log.Info(bininfo.StringifySingleLine()) filename, urlTmpl, num, isRecursive := parseFlag() urls := collect(urlTmpl, num) tags := readAllTag(filename) log.Debug(urls, num) push(tags, urls, isRecursive) log.Info("bye.") } // readAllTag 预读取 flv 文件中的所有 tag,缓存在内存中 func readAllTag(filename string) (ret []httpflv.Tag) { var ffr httpflv.FLVFileReader err := ffr.Open(filename) log.Assert(nil, err) log.Infof("open succ. filename=%s", filename) for { tag, err := ffr.ReadTag() if err == io.EOF { log.Info("EOF") break } if err != nil { log.Info(err) break } if tag.IsMetadata() { log.Debugf("M %d", tag.Header.Timestamp) } else if tag.IsVideoKeySeqHeader() { log.Debugf("V SH %d", tag.Header.Timestamp) } else if tag.IsVideoKeyNalu() { log.Debugf("V K %d", tag.Header.Timestamp) } else if tag.IsAACSeqHeader() { log.Debugf("A SH %d", tag.Header.Timestamp) } ret = append(ret, tag) } log.Infof("read all tag done. num=%d", len(ret)) return } func push(tags []httpflv.Tag, urls []string, isRecursive bool) { if len(tags) == 0 || len(urls) == 0 { return } var err error var psList []*rtmp.PushSession for i := range urls { ps := rtmp.NewPushSession(func(option *rtmp.PushSessionOption) { option.ConnectTimeoutMS = 3000 option.PushTimeoutMS = 5000 option.WriteAVTimeoutMS = 10000 }) err = ps.Push(urls[i]) log.Assert(nil, err) log.Infof("push succ. url=%s", urls[i]) psList = append(psList, ps) } var totalBaseTS uint32 var prevTS uint32 var hasReadThisBaseTS bool var thisBaseTS uint32 var hasTraceFirstTagTS bool var firstTagTS uint32 var firstTagTick int64 for i := 0; ; i++ { log.Infof(" > round. i=%d, totalBaseTS=%d, prevTS=%d, thisBaseTS=%d", i, totalBaseTS, prevTS, thisBaseTS) hasReadThisBaseTS = false for _, tag := range tags { h := logic.Trans.FLVTagHeader2RTMPHeader(tag.Header) if tag.IsMetadata() { if totalBaseTS == 0 { // 第一个metadata直接发送 h.TimestampAbs = 0 chunks := rtmp.Message2Chunks(tag.Raw[11:11+h.MsgLen], &h) for _, ps := range psList { err = ps.AsyncWrite(chunks) log.Assert(nil, err) } } else { // noop } continue } if hasReadThisBaseTS { // 之前已经读到了这轮读文件的base值,ts要减去base h.TimestampAbs = tag.Header.Timestamp - thisBaseTS + totalBaseTS } else { // 设置base,ts设置为上一轮读文件的值 thisBaseTS = tag.Header.Timestamp h.TimestampAbs = totalBaseTS hasReadThisBaseTS = true } if h.TimestampAbs < prevTS { // ts比上一个包的还小,直接设置为上一包的值,并且不sleep直接发送 h.TimestampAbs = prevTS } chunks := rtmp.Message2Chunks(tag.Raw[11:11+h.MsgLen], &h) if hasTraceFirstTagTS { n := time.Now().UnixNano() / 1000000 diffTick := n - firstTagTick diffTS := h.TimestampAbs - firstTagTS if diffTick < int64(diffTS) { time.Sleep(time.Duration(int64(diffTS)-diffTick) * time.Millisecond) } } else { firstTagTick = time.Now().UnixNano() / 1000000 firstTagTS = h.TimestampAbs hasTraceFirstTagTS = true } for _, ps := range psList { err = ps.AsyncWrite(chunks) log.Assert(nil, err) } prevTS = h.TimestampAbs } totalBaseTS = prevTS + 1 if !isRecursive { break } } } func collect(urlTmpl string, num int) (urls []string) { for i := 0; i < num; i++ { url := strings.Replace(urlTmpl, "{i}", strconv.Itoa(i), -1) urls = append(urls, url) } return } func parseFlag() (filename string, urlTmpl string, num int, isRecursive bool) { v := flag.Bool("v", false, "show bin info") i := flag.String("i", "", "specify flv file") o := flag.String("o", "", "specify rtmp push url") r := flag.Bool("r", false, "recursive push if reach end of file") n := flag.Int("n", 1, "num of push connection") flag.Parse() if *v { _, _ = fmt.Fprint(os.Stderr, bininfo.StringifyMultiLine()) os.Exit(1) } if *i == "" || *o == "" { flag.Usage() _, _ = fmt.Fprintf(os.Stderr, `Example: ./bin/flvfile2rtmppush -i testdata/test.flv -o rtmp://127.0.0.1:19350/live/test ./bin/flvfile2rtmppush -i testdata/test.flv -o rtmp://127.0.0.1:19350/live/test -r ./bin/flvfile2rtmppush -i testdata/test.flv -o rtmp://127.0.0.1:19350/live/test_{i} -r -n 1000 `) os.Exit(1) } return *i, *o, *n, *r }