|
|
// 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"
|
|
|
"os"
|
|
|
"strconv"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
"github.com/q191201771/naza/pkg/nazastring"
|
|
|
|
|
|
"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流的时间戳。注意,这个程序还没有完成。
|
|
|
// 功能:
|
|
|
// - 时间戳回退检查
|
|
|
// - 当音频时间戳出现回退时打error日志
|
|
|
// - 当视频时间戳出现回退时打error日志
|
|
|
// - 将音频和视频时间戳看成一个整体,出现回退时打error日志
|
|
|
// - 定时打印:
|
|
|
// - 总体带宽
|
|
|
// - 音频带宽
|
|
|
// - 视频带宽
|
|
|
// - 视频DTS和PTS不相等的计数
|
|
|
// - I帧间隔时间
|
|
|
// - H264
|
|
|
// - 打印每个tag的类型:key seq header...
|
|
|
// - 打印每个tag中有多少个帧:SPS PPS SEI IDR SLICE...
|
|
|
// - 打印每个SLICE的类型:I、P、B...
|
|
|
// 解析metadata信息,并打印
|
|
|
|
|
|
// TODO
|
|
|
// - 检查时间戳正向大的跳跃
|
|
|
// - 打印GOP中帧数量?
|
|
|
// - slice_num?
|
|
|
// - 输入源可以是httpflv,也可以是flv文件
|
|
|
|
|
|
var (
|
|
|
timestampCheckFlag = true
|
|
|
printStatFlag = true
|
|
|
printEveryTagFlag = false
|
|
|
printMetaData = true
|
|
|
analysisVideoTagFlag = true
|
|
|
)
|
|
|
|
|
|
var (
|
|
|
prevAudioTS = int64(-1)
|
|
|
prevVideoTS = int64(-1)
|
|
|
prevTS = int64(-1)
|
|
|
prevIDRTS = int64(-1)
|
|
|
diffIDRTS = int64(-1)
|
|
|
)
|
|
|
|
|
|
func main() {
|
|
|
url := parseFlag()
|
|
|
session := httpflv.NewPullSession()
|
|
|
|
|
|
brTotal := bitrate.New()
|
|
|
brAudio := bitrate.New()
|
|
|
brVideo := bitrate.New()
|
|
|
|
|
|
videoCTSNotZeroCount := 0
|
|
|
|
|
|
go func() {
|
|
|
for {
|
|
|
time.Sleep(1 * 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)
|
|
|
}
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
err := session.Pull(url, func(tag httpflv.Tag) {
|
|
|
if printEveryTagFlag {
|
|
|
debugLength := 32
|
|
|
if len(tag.Raw) < 32 {
|
|
|
debugLength = len(tag.Raw)
|
|
|
}
|
|
|
nazalog.Debugf("header=%+v, hex=%s", tag.Header, hex.Dump(tag.Raw[11:debugLength]))
|
|
|
}
|
|
|
|
|
|
brTotal.Add(len(tag.Raw))
|
|
|
|
|
|
switch tag.Header.Type {
|
|
|
case httpflv.TagTypeMetadata:
|
|
|
if printMetaData {
|
|
|
nazalog.Debugf("----------\n%s", hex.Dump(tag.Raw[11:]))
|
|
|
|
|
|
opa, err := rtmp.ParseMetadata(tag.Raw[11 : len(tag.Raw)-4])
|
|
|
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:
|
|
|
brAudio.Add(len(tag.Raw))
|
|
|
|
|
|
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:
|
|
|
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)
|
|
|
}
|
|
|
})
|
|
|
nazalog.Warn(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] ")
|
|
|
if _, _, err := avc.ParseSPSPPSFromSeqHeader(tag.Raw[11:]); err != nil {
|
|
|
buf.WriteString(" parse sps pps failed.")
|
|
|
}
|
|
|
} else if tag.IsHEVCKeySeqHeader() {
|
|
|
t = typeHEVC
|
|
|
//nazalog.Debugf("%s", nazastring.DumpSliceByte(tag.Raw[11:]))
|
|
|
vps, sps, pps, _ := hevc.ParseVPSSPSPPSFromSeqHeader(tag.Raw[11:])
|
|
|
nazalog.Debugf("%s", nazastring.DumpSliceByte(vps))
|
|
|
nazalog.Debugf("%s", nazastring.DumpSliceByte(sps))
|
|
|
nazalog.Debugf("%s", nazastring.DumpSliceByte(pps))
|
|
|
//nazalog.Debugf("%s %s %s %+v", hex.Dump(vps), hex.Dump(sps), hex.Dump(pps), err)
|
|
|
buf.WriteString(" [HEVC SeqHeader] ")
|
|
|
}
|
|
|
} else {
|
|
|
body := tag.Raw[11:]
|
|
|
|
|
|
i := 5
|
|
|
for i != int(tag.Header.DataSize) {
|
|
|
if i+4 > int(tag.Header.DataSize) {
|
|
|
nazalog.Errorf("invalid nalu size. i=%d, tag size=%d", i, int(tag.Header.DataSize))
|
|
|
break
|
|
|
}
|
|
|
naluLen := bele.BEUint32(body[i:])
|
|
|
if i+int(naluLen) > int(tag.Header.DataSize) {
|
|
|
nazalog.Errorf("invalid nalu size. i=%d, naluLen=%d, tag size=%d", i, naluLen, int(tag.Header.DataSize))
|
|
|
break
|
|
|
}
|
|
|
switch t {
|
|
|
case typeAVC:
|
|
|
if avc.ParseNALUType(body[i+4]) == avc.NALUTypeIDRSlice {
|
|
|
if prevIDRTS != int64(-1) {
|
|
|
diffIDRTS = int64(tag.Header.Timestamp) - prevIDRTS
|
|
|
}
|
|
|
prevIDRTS = int64(tag.Header.Timestamp)
|
|
|
}
|
|
|
if avc.ParseNALUType(body[i+4]) == avc.NALUTypeSEI {
|
|
|
delay := SEIDelayMS(body[i+4 : i+4+int(naluLen)])
|
|
|
if delay != -1 {
|
|
|
buf.WriteString(fmt.Sprintf("delay: %dms", delay))
|
|
|
}
|
|
|
}
|
|
|
sliceTypeReadable, _ := avc.ParseSliceTypeReadable(body[i+4:])
|
|
|
buf.WriteString(fmt.Sprintf(" [%s(%s)] ", avc.ParseNALUTypeReadable(body[i+4]), sliceTypeReadable))
|
|
|
case typeHEVC:
|
|
|
if hevc.ParseNALUType(body[i+4]) == hevc.NALUTypeSEI {
|
|
|
delay := SEIDelayMS(body[i+4 : i+4+int(naluLen)])
|
|
|
if delay != -1 {
|
|
|
buf.WriteString(fmt.Sprintf("delay: %dms", delay))
|
|
|
}
|
|
|
}
|
|
|
buf.WriteString(fmt.Sprintf(" [%s] ", hevc.ParseNALUTypeReadable(body[i+4])))
|
|
|
}
|
|
|
i = i + 4 + int(naluLen)
|
|
|
}
|
|
|
}
|
|
|
if analysisVideoTagFlag {
|
|
|
nazalog.Debug(buf.String())
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 注意,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 {
|
|
|
url := flag.String("i", "", "specify http-flv url")
|
|
|
flag.Parse()
|
|
|
if *url == "" {
|
|
|
flag.Usage()
|
|
|
os.Exit(1)
|
|
|
}
|
|
|
return *url
|
|
|
}
|