|
|
// 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 (
|
|
|
"bytes"
|
|
|
"encoding/hex"
|
|
|
"flag"
|
|
|
"fmt"
|
|
|
"github.com/q191201771/lal/pkg/aac"
|
|
|
"strconv"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
"github.com/q191201771/naza/pkg/nazabytes"
|
|
|
|
|
|
"github.com/q191201771/lal/pkg/base"
|
|
|
|
|
|
"github.com/q191201771/lal/pkg/rtmp"
|
|
|
|
|
|
"github.com/q191201771/lal/pkg/avc"
|
|
|
"github.com/q191201771/lal/pkg/hevc"
|
|
|
"github.com/q191201771/naza/pkg/bele"
|
|
|
"github.com/q191201771/naza/pkg/bitrate"
|
|
|
|
|
|
"github.com/q191201771/lal/pkg/httpflv"
|
|
|
"github.com/q191201771/naza/pkg/nazalog"
|
|
|
)
|
|
|
|
|
|
// 分析诊断HTTP-FLV流以及FLV文件的小工具。
|
|
|
//
|
|
|
// 功能:
|
|
|
// - 时间戳回退检查
|
|
|
// - 当音频时间戳出现回退时打error日志
|
|
|
// - 当视频时间戳出现回退时打error日志
|
|
|
// - 将音频和视频时间戳看成一个整体,出现回退时打error日志
|
|
|
// - 定时打印:
|
|
|
// - 总体带宽
|
|
|
// - 音频带宽
|
|
|
// - 视频带宽
|
|
|
// - 视频DTS和PTS不相等的计数
|
|
|
// - I帧间隔时间
|
|
|
// - metadata
|
|
|
// - H264
|
|
|
// - 打印每个tag的类型:key seq header...
|
|
|
// - 打印每个tag中有多少个帧:SPS PPS SEI IDR SLICE...
|
|
|
// - 打印每个SLICE的类型:I、P、B...
|
|
|
// - AAC
|
|
|
// - 解析seq header
|
|
|
//
|
|
|
|
|
|
// TODO
|
|
|
// - 检查时间戳正向大的跳跃
|
|
|
// - 打印GOP中帧数量?
|
|
|
// - slice_num?
|
|
|
|
|
|
var (
|
|
|
timestampCheckFlag = true
|
|
|
printStatFlag = true
|
|
|
printEveryTagFlag = true
|
|
|
printMetaData = true
|
|
|
analysisVideoTagFlag = true
|
|
|
)
|
|
|
|
|
|
var (
|
|
|
prevAudioTs = int64(-1)
|
|
|
prevVideoTs = int64(-1)
|
|
|
prevTs = int64(-1)
|
|
|
prevIdrTs = int64(-1)
|
|
|
diffIdrTs = int64(-1)
|
|
|
)
|
|
|
|
|
|
var brTotal = bitrate.New(func(option *bitrate.Option) {
|
|
|
option.WindowMs = 5000
|
|
|
})
|
|
|
|
|
|
var brAudio = bitrate.New(func(option *bitrate.Option) {
|
|
|
option.WindowMs = 5000
|
|
|
})
|
|
|
|
|
|
var brVideo = bitrate.New(func(option *bitrate.Option) {
|
|
|
option.WindowMs = 5000
|
|
|
})
|
|
|
|
|
|
var videoCtsNotZeroCount = 0
|
|
|
|
|
|
func handleTags(tag httpflv.Tag) bool {
|
|
|
if printEveryTagFlag {
|
|
|
nazalog.Debugf("header=%+v, hex=%s", tag.Header, hex.Dump(nazabytes.Prefix(tag.Payload(), 32)))
|
|
|
}
|
|
|
|
|
|
brTotal.Add(len(tag.Raw))
|
|
|
|
|
|
switch tag.Header.Type {
|
|
|
case httpflv.TagTypeMetadata:
|
|
|
if printMetaData {
|
|
|
nazalog.Debugf("----------\n%s", hex.Dump(tag.Payload()))
|
|
|
|
|
|
opa, err := rtmp.ParseMetadata(tag.Payload())
|
|
|
nazalog.Assert(nil, err)
|
|
|
var buf bytes.Buffer
|
|
|
buf.WriteString(fmt.Sprintf("-----\ncount:%d\n", len(opa)))
|
|
|
for _, op := range opa {
|
|
|
buf.WriteString(fmt.Sprintf(" %s: %+v\n", op.Key, op.Value))
|
|
|
}
|
|
|
nazalog.Debugf("%+v", buf.String())
|
|
|
}
|
|
|
case httpflv.TagTypeAudio:
|
|
|
//nazalog.Debugf("header=%+v, body=%s", tag.Header, hex.Dump(nazabytes.Prefix(tag.Payload(), 128)))
|
|
|
brAudio.Add(len(tag.Raw))
|
|
|
|
|
|
if tag.IsAacSeqHeader() {
|
|
|
ascCtx, err := aac.NewAscContext(tag.Payload()[2:])
|
|
|
nazalog.Assert(nil, err)
|
|
|
nazalog.Infof("aac seq header. %s, %+v", hex.EncodeToString(tag.Payload()), ascCtx)
|
|
|
}
|
|
|
if timestampCheckFlag {
|
|
|
if prevAudioTs != -1 && int64(tag.Header.Timestamp) < prevAudioTs {
|
|
|
nazalog.Errorf("audio timestamp error, less than prev audio timestamp. header=%+v, prevAudioTs=%d, diff=%d", tag.Header, prevAudioTs, int64(tag.Header.Timestamp)-prevAudioTs)
|
|
|
}
|
|
|
if prevTs != -1 && int64(tag.Header.Timestamp) < prevTs {
|
|
|
nazalog.Warnf("audio timestamp error. less than prev global timestamp. header=%+v, prevTs=%d, diff=%d", tag.Header, prevTs, int64(tag.Header.Timestamp)-prevTs)
|
|
|
}
|
|
|
}
|
|
|
prevAudioTs = int64(tag.Header.Timestamp)
|
|
|
prevTs = int64(tag.Header.Timestamp)
|
|
|
case httpflv.TagTypeVideo:
|
|
|
nazalog.Debugf("header=%+v, body=%s", tag.Header, hex.Dump(nazabytes.Prefix(tag.Payload(), 128)))
|
|
|
analysisVideoTag(tag)
|
|
|
|
|
|
videoCts := bele.BeUint24(tag.Raw[13:])
|
|
|
if videoCts != 0 {
|
|
|
videoCtsNotZeroCount++
|
|
|
}
|
|
|
|
|
|
brVideo.Add(len(tag.Raw))
|
|
|
|
|
|
if timestampCheckFlag {
|
|
|
if prevVideoTs != -1 && int64(tag.Header.Timestamp) < prevVideoTs {
|
|
|
nazalog.Errorf("video timestamp error, less than prev video timestamp. header=%+v, prevVideoTs=%d, diff=%d", tag.Header, prevVideoTs, int64(tag.Header.Timestamp)-prevVideoTs)
|
|
|
}
|
|
|
if prevTs != -1 && int64(tag.Header.Timestamp) < prevTs {
|
|
|
nazalog.Warnf("video timestamp error, less than prev global timestamp. header=%+v, prevTs=%d, diff=%d", tag.Header, prevTs, int64(tag.Header.Timestamp)-prevTs)
|
|
|
}
|
|
|
}
|
|
|
prevVideoTs = int64(tag.Header.Timestamp)
|
|
|
prevTs = int64(tag.Header.Timestamp)
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
func main() {
|
|
|
_ = nazalog.Init(func(option *nazalog.Option) {
|
|
|
option.AssertBehavior = nazalog.AssertFatal
|
|
|
})
|
|
|
defer nazalog.Sync()
|
|
|
base.LogoutStartInfo()
|
|
|
|
|
|
in := parseFlag()
|
|
|
|
|
|
go func() {
|
|
|
for {
|
|
|
time.Sleep(5 * time.Second)
|
|
|
if printStatFlag {
|
|
|
nazalog.Debugf("stat. total=%dKb/s, audio=%dKb/s, video=%dKb/s, videoCtsNotZeroCount=%d, diffIdrTs=%d",
|
|
|
int(brTotal.Rate()), int(brAudio.Rate()), int(brVideo.Rate()), videoCtsNotZeroCount, diffIdrTs)
|
|
|
}
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
if strings.HasPrefix(in, "http") || strings.HasPrefix(in, "https") {
|
|
|
session := httpflv.NewPullSession().WithOnReadFlvTag(func(tag httpflv.Tag) {
|
|
|
handleTags(tag)
|
|
|
})
|
|
|
|
|
|
// TODO(chef): [refactor] 统一 PullSession 和 FilePump 的回调格式 202211
|
|
|
err := session.Start(in)
|
|
|
nazalog.Assert(nil, err)
|
|
|
|
|
|
// 临时测试一下主动关闭client session
|
|
|
//go func() {
|
|
|
// time.Sleep(5 * time.Second)
|
|
|
// _ = session.Dispose()
|
|
|
//}()
|
|
|
|
|
|
err = <-session.WaitChan()
|
|
|
nazalog.Errorf("< session.WaitChan. err=%+v", err)
|
|
|
} else {
|
|
|
err := httpflv.NewFlvFilePump().Pump(in, handleTags)
|
|
|
nazalog.Assert(nil, err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const (
|
|
|
typeUnknown uint8 = 1
|
|
|
typeAvc uint8 = 2
|
|
|
typeHevc uint8 = 3
|
|
|
)
|
|
|
|
|
|
var t uint8 = typeUnknown
|
|
|
|
|
|
func analysisVideoTag(tag httpflv.Tag) {
|
|
|
var buf bytes.Buffer
|
|
|
if tag.IsVideoKeySeqHeader() {
|
|
|
if tag.IsAvcKeySeqHeader() {
|
|
|
t = typeAvc
|
|
|
buf.WriteString(" [AVC SeqHeader] ")
|
|
|
sps, pps, err := avc.ParseSpsPpsFromSeqHeader(tag.Payload())
|
|
|
if err != nil {
|
|
|
buf.WriteString(" parse sps pps failed.")
|
|
|
}
|
|
|
nazalog.Debugf("sps:%s, pps:%s", hex.Dump(sps), hex.Dump(pps))
|
|
|
} else if tag.IsHevcKeySeqHeader() {
|
|
|
t = typeHevc
|
|
|
buf.WriteString(" [HEVC SeqHeader] ")
|
|
|
buf.WriteString(hex.Dump(tag.Payload()))
|
|
|
if _, _, _, err := hevc.ParseVpsSpsPpsFromSeqHeader(tag.Payload()); err != nil {
|
|
|
buf.WriteString(" parse vps sps pps failed.")
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
cts := bele.BeUint24(tag.Payload()[2:])
|
|
|
buf.WriteString(fmt.Sprintf("%+v, cts=%d, pts=%d", tag.Header, cts, tag.Header.Timestamp+cts))
|
|
|
|
|
|
body := tag.Payload()[5:]
|
|
|
nals, err := avc.SplitNaluAvcc(body)
|
|
|
nazalog.Assert(nil, err)
|
|
|
|
|
|
for _, nal := range nals {
|
|
|
switch t {
|
|
|
case typeAvc:
|
|
|
if avc.ParseNaluType(nal[0]) == avc.NaluTypeIdrSlice {
|
|
|
nazalog.Debugf("IDR:%s", hex.Dump(nazabytes.Prefix(nal, 128)))
|
|
|
if prevIdrTs != int64(-1) {
|
|
|
diffIdrTs = int64(tag.Header.Timestamp) - prevIdrTs
|
|
|
}
|
|
|
prevIdrTs = int64(tag.Header.Timestamp)
|
|
|
}
|
|
|
if avc.ParseNaluType(nal[0]) == avc.NaluTypeSei {
|
|
|
delay := SeiDelayMs(nal)
|
|
|
if delay != -1 {
|
|
|
buf.WriteString(fmt.Sprintf("delay: %dms", delay))
|
|
|
}
|
|
|
}
|
|
|
sliceTypeReadable, _ := avc.ParseSliceTypeReadable(nal)
|
|
|
buf.WriteString(fmt.Sprintf(" [%s(%s)(%d)] ", avc.ParseNaluTypeReadable(nal[0]), sliceTypeReadable, len(nal)))
|
|
|
case typeHevc:
|
|
|
if hevc.ParseNaluType(nal[0]) == hevc.NaluTypeSei {
|
|
|
delay := SeiDelayMs(nal)
|
|
|
if delay != -1 {
|
|
|
buf.WriteString(fmt.Sprintf("delay: %dms", delay))
|
|
|
}
|
|
|
}
|
|
|
buf.WriteString(fmt.Sprintf(" [%s(%d)] ", hevc.ParseNaluTypeReadable(nal[0]), nal[0]))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
if analysisVideoTagFlag {
|
|
|
nazalog.Debug(buf.String())
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// SeiDelayMs 注意,SEI的内容是自定义格式,解析的代码不具有通用性
|
|
|
func SeiDelayMs(seiNalu []byte) int {
|
|
|
//nazalog.Debugf("sei: %s", hex.Dump(seiNalu))
|
|
|
items := strings.Split(string(seiNalu), ":")
|
|
|
if len(items) != 3 {
|
|
|
return -1
|
|
|
}
|
|
|
|
|
|
a, err := strconv.ParseInt(items[1], 10, 64)
|
|
|
if err != nil {
|
|
|
return -1
|
|
|
}
|
|
|
t := time.Unix(a/1e3, a%1e3)
|
|
|
d := time.Now().Sub(t)
|
|
|
return int(d.Nanoseconds() / 1e6)
|
|
|
}
|
|
|
|
|
|
func parseFlag() string {
|
|
|
in := flag.String("i", "", "specify http-flv url, or flv filename")
|
|
|
flag.Parse()
|
|
|
if *in == "" {
|
|
|
flag.Usage()
|
|
|
base.OsExitAndWaitPressIfWindows(1)
|
|
|
}
|
|
|
return *in
|
|
|
}
|