// 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
}