506 lines
11 KiB
Go
506 lines
11 KiB
Go
package tutk
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
|
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
|
)
|
|
|
|
const (
|
|
FrameTypeStart uint8 = 0x08 // Extended start (36-byte header)
|
|
FrameTypeStartAlt uint8 = 0x09 // StartAlt (36-byte header)
|
|
FrameTypeCont uint8 = 0x00 // Continuation (28-byte header)
|
|
FrameTypeContAlt uint8 = 0x04 // Continuation alt
|
|
FrameTypeEndSingle uint8 = 0x01 // Single-packet frame (28-byte)
|
|
FrameTypeEndMulti uint8 = 0x05 // Multi-packet end (28-byte)
|
|
FrameTypeEndExt uint8 = 0x0d // Extended end (36-byte)
|
|
)
|
|
|
|
const (
|
|
ChannelIVideo uint8 = 0x05
|
|
ChannelAudio uint8 = 0x03
|
|
ChannelPVideo uint8 = 0x07
|
|
)
|
|
|
|
// Resolution constants
|
|
const (
|
|
ResolutionUnknown = 0
|
|
ResolutionSD = 1
|
|
Resolution360P = 2
|
|
Resolution2K = 4
|
|
)
|
|
|
|
const FrameInfoSize = 40
|
|
|
|
// FrameInfo - Wyze extended FRAMEINFO (40 bytes at end of packet)
|
|
type FrameInfo struct {
|
|
CodecID uint16
|
|
Flags uint8
|
|
CamIndex uint8
|
|
OnlineNum uint8
|
|
Framerate uint8
|
|
FrameSize uint8
|
|
Bitrate uint8
|
|
TimestampUS uint32
|
|
Timestamp uint32
|
|
PayloadSize uint32
|
|
FrameNo uint32
|
|
}
|
|
|
|
func (fi *FrameInfo) IsKeyframe() bool {
|
|
return fi.Flags == 0x01
|
|
}
|
|
|
|
func (fi *FrameInfo) Resolution() string {
|
|
switch fi.FrameSize {
|
|
case ResolutionSD:
|
|
return "SD"
|
|
case Resolution360P:
|
|
return "360P"
|
|
case Resolution2K:
|
|
return "2K"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func (fi *FrameInfo) SampleRate() uint32 {
|
|
idx := (fi.Flags >> 2) & 0x0F
|
|
return uint32(SampleRateValue(idx))
|
|
}
|
|
|
|
func (fi *FrameInfo) Channels() uint8 {
|
|
if fi.Flags&0x01 == 1 {
|
|
return 2
|
|
}
|
|
return 1
|
|
}
|
|
|
|
func (fi *FrameInfo) IsVideo() bool {
|
|
return IsVideoCodec(fi.CodecID)
|
|
}
|
|
|
|
func (fi *FrameInfo) IsAudio() bool {
|
|
return IsAudioCodec(fi.CodecID)
|
|
}
|
|
|
|
func ParseFrameInfo(data []byte) *FrameInfo {
|
|
if len(data) < FrameInfoSize {
|
|
return nil
|
|
}
|
|
|
|
offset := len(data) - FrameInfoSize
|
|
fi := data[offset:]
|
|
|
|
return &FrameInfo{
|
|
CodecID: binary.LittleEndian.Uint16(fi),
|
|
Flags: fi[2],
|
|
CamIndex: fi[3],
|
|
OnlineNum: fi[4],
|
|
Framerate: fi[5],
|
|
FrameSize: fi[6],
|
|
Bitrate: fi[7],
|
|
TimestampUS: binary.LittleEndian.Uint32(fi[8:]),
|
|
Timestamp: binary.LittleEndian.Uint32(fi[12:]),
|
|
PayloadSize: binary.LittleEndian.Uint32(fi[16:]),
|
|
FrameNo: binary.LittleEndian.Uint32(fi[20:]),
|
|
}
|
|
}
|
|
|
|
type Packet struct {
|
|
Channel uint8
|
|
Codec uint16
|
|
Timestamp uint32
|
|
Payload []byte
|
|
IsKeyframe bool
|
|
FrameNo uint32
|
|
SampleRate uint32
|
|
Channels uint8
|
|
}
|
|
|
|
func (p *Packet) IsVideo() bool {
|
|
return p.Channel == ChannelIVideo || p.Channel == ChannelPVideo
|
|
}
|
|
|
|
func (p *Packet) IsAudio() bool {
|
|
return p.Channel == ChannelAudio
|
|
}
|
|
|
|
type PacketHeader struct {
|
|
Channel byte
|
|
FrameType byte
|
|
HeaderSize int
|
|
FrameNo uint32
|
|
PktIdx uint16
|
|
PktTotal uint16
|
|
PayloadSize uint16
|
|
HasFrameInfo bool
|
|
}
|
|
|
|
func ParsePacketHeader(data []byte) *PacketHeader {
|
|
if len(data) < 28 {
|
|
return nil
|
|
}
|
|
|
|
frameType := data[1]
|
|
hdr := &PacketHeader{
|
|
Channel: data[0],
|
|
FrameType: frameType,
|
|
}
|
|
|
|
switch frameType {
|
|
case FrameTypeStart, FrameTypeStartAlt, FrameTypeEndExt:
|
|
hdr.HeaderSize = 36
|
|
default:
|
|
hdr.HeaderSize = 28
|
|
}
|
|
|
|
if len(data) < hdr.HeaderSize {
|
|
return nil
|
|
}
|
|
|
|
if hdr.HeaderSize == 28 {
|
|
hdr.PktTotal = binary.LittleEndian.Uint16(data[12:])
|
|
pktIdxOrMarker := binary.LittleEndian.Uint16(data[14:])
|
|
hdr.PayloadSize = binary.LittleEndian.Uint16(data[16:])
|
|
hdr.FrameNo = binary.LittleEndian.Uint32(data[24:])
|
|
|
|
if IsEndFrame(frameType) && pktIdxOrMarker == 0x0028 {
|
|
hdr.HasFrameInfo = true
|
|
if hdr.PktTotal > 0 {
|
|
hdr.PktIdx = hdr.PktTotal - 1
|
|
}
|
|
} else {
|
|
hdr.PktIdx = pktIdxOrMarker
|
|
}
|
|
} else {
|
|
hdr.PktTotal = binary.LittleEndian.Uint16(data[20:])
|
|
pktIdxOrMarker := binary.LittleEndian.Uint16(data[22:])
|
|
hdr.PayloadSize = binary.LittleEndian.Uint16(data[24:])
|
|
hdr.FrameNo = binary.LittleEndian.Uint32(data[32:])
|
|
|
|
if IsEndFrame(frameType) && pktIdxOrMarker == 0x0028 {
|
|
hdr.HasFrameInfo = true
|
|
if hdr.PktTotal > 0 {
|
|
hdr.PktIdx = hdr.PktTotal - 1
|
|
}
|
|
} else {
|
|
hdr.PktIdx = pktIdxOrMarker
|
|
}
|
|
}
|
|
|
|
return hdr
|
|
}
|
|
|
|
func IsStartFrame(frameType uint8) bool {
|
|
return frameType == FrameTypeStart || frameType == FrameTypeStartAlt
|
|
}
|
|
|
|
func IsEndFrame(frameType uint8) bool {
|
|
return frameType == FrameTypeEndSingle ||
|
|
frameType == FrameTypeEndMulti ||
|
|
frameType == FrameTypeEndExt
|
|
}
|
|
|
|
func IsContinuationFrame(frameType uint8) bool {
|
|
return frameType == FrameTypeCont || frameType == FrameTypeContAlt
|
|
}
|
|
|
|
type FrameAssembler struct {
|
|
FrameNo uint32
|
|
PktTotal uint16
|
|
Packets map[uint16][]byte
|
|
FrameInfo *FrameInfo
|
|
}
|
|
|
|
func ParseAudioParams(payload []byte, fi *FrameInfo) (sampleRate uint32, channels uint8) {
|
|
if aac.IsADTS(payload) {
|
|
codec := aac.ADTSToCodec(payload)
|
|
if codec != nil {
|
|
return codec.ClockRate, codec.Channels
|
|
}
|
|
}
|
|
|
|
if fi != nil {
|
|
return fi.SampleRate(), fi.Channels()
|
|
}
|
|
|
|
return 16000, 1
|
|
}
|
|
|
|
type FrameHandler struct {
|
|
assemblers map[byte]*FrameAssembler
|
|
baseTS uint64
|
|
output chan *Packet
|
|
verbose bool
|
|
}
|
|
|
|
func NewFrameHandler(verbose bool) *FrameHandler {
|
|
return &FrameHandler{
|
|
assemblers: make(map[byte]*FrameAssembler),
|
|
output: make(chan *Packet, 128),
|
|
verbose: verbose,
|
|
}
|
|
}
|
|
|
|
func (h *FrameHandler) Recv() <-chan *Packet {
|
|
return h.output
|
|
}
|
|
|
|
func (h *FrameHandler) Close() {
|
|
close(h.output)
|
|
}
|
|
|
|
func (h *FrameHandler) Handle(data []byte) {
|
|
hdr := ParsePacketHeader(data)
|
|
if hdr == nil {
|
|
return
|
|
}
|
|
|
|
if h.verbose {
|
|
h.logWireHeader(data, hdr)
|
|
}
|
|
|
|
payload, fi := h.extractPayload(data, hdr.Channel)
|
|
if payload == nil {
|
|
return
|
|
}
|
|
|
|
if h.verbose {
|
|
h.logAVPacket(hdr.Channel, hdr.FrameType, payload, fi)
|
|
}
|
|
|
|
switch hdr.Channel {
|
|
case ChannelAudio:
|
|
h.handleAudio(payload, fi)
|
|
case ChannelIVideo, ChannelPVideo:
|
|
h.handleVideo(hdr.Channel, hdr, payload, fi)
|
|
}
|
|
}
|
|
|
|
func (h *FrameHandler) extractPayload(data []byte, channel byte) ([]byte, *FrameInfo) {
|
|
if len(data) < 2 {
|
|
return nil, nil
|
|
}
|
|
|
|
frameType := data[1]
|
|
|
|
headerSize := 28
|
|
frameInfoSize := 0
|
|
|
|
switch frameType {
|
|
case FrameTypeStart:
|
|
headerSize = 36
|
|
case FrameTypeStartAlt:
|
|
headerSize = 36
|
|
if len(data) >= 22 {
|
|
pktTotal := binary.LittleEndian.Uint16(data[20:])
|
|
if pktTotal == 1 {
|
|
frameInfoSize = FrameInfoSize
|
|
}
|
|
}
|
|
case FrameTypeCont, FrameTypeContAlt:
|
|
headerSize = 28
|
|
case FrameTypeEndSingle, FrameTypeEndMulti:
|
|
headerSize = 28
|
|
frameInfoSize = FrameInfoSize
|
|
case FrameTypeEndExt:
|
|
headerSize = 36
|
|
frameInfoSize = FrameInfoSize
|
|
default:
|
|
headerSize = 28
|
|
}
|
|
|
|
if len(data) < headerSize {
|
|
return nil, nil
|
|
}
|
|
|
|
if frameInfoSize == 0 {
|
|
return data[headerSize:], nil
|
|
}
|
|
|
|
if len(data) < headerSize+frameInfoSize {
|
|
return data[headerSize:], nil
|
|
}
|
|
|
|
fi := ParseFrameInfo(data)
|
|
|
|
validCodec := false
|
|
switch channel {
|
|
case ChannelIVideo, ChannelPVideo:
|
|
validCodec = IsVideoCodec(fi.CodecID)
|
|
case ChannelAudio:
|
|
validCodec = IsAudioCodec(fi.CodecID)
|
|
}
|
|
|
|
if validCodec {
|
|
payload := data[headerSize : len(data)-frameInfoSize]
|
|
return payload, fi
|
|
}
|
|
|
|
return data[headerSize:], nil
|
|
}
|
|
|
|
func (h *FrameHandler) handleVideo(channel byte, hdr *PacketHeader, payload []byte, fi *FrameInfo) {
|
|
asm := h.assemblers[channel]
|
|
|
|
// Frame transition: new frame number = previous frame complete
|
|
if asm != nil && hdr.FrameNo != asm.FrameNo {
|
|
gotAll := uint16(len(asm.Packets)) == asm.PktTotal
|
|
if gotAll && asm.FrameInfo != nil {
|
|
h.assembleAndQueue(channel, asm)
|
|
}
|
|
asm = nil
|
|
}
|
|
|
|
// Create new assembler if needed
|
|
if asm == nil {
|
|
asm = &FrameAssembler{
|
|
FrameNo: hdr.FrameNo,
|
|
PktTotal: hdr.PktTotal,
|
|
Packets: make(map[uint16][]byte, hdr.PktTotal),
|
|
}
|
|
h.assemblers[channel] = asm
|
|
}
|
|
|
|
// Store packet (copy payload - buffer is reused by worker)
|
|
payloadCopy := make([]byte, len(payload))
|
|
copy(payloadCopy, payload)
|
|
asm.Packets[hdr.PktIdx] = payloadCopy
|
|
|
|
if fi != nil {
|
|
asm.FrameInfo = fi
|
|
}
|
|
|
|
// Check if frame is complete
|
|
if uint16(len(asm.Packets)) == asm.PktTotal && asm.FrameInfo != nil {
|
|
h.assembleAndQueue(channel, asm)
|
|
delete(h.assemblers, channel)
|
|
}
|
|
}
|
|
|
|
func (h *FrameHandler) assembleAndQueue(channel byte, asm *FrameAssembler) {
|
|
fi := asm.FrameInfo
|
|
|
|
// Assemble packets in correct order
|
|
var payload []byte
|
|
for i := uint16(0); i < asm.PktTotal; i++ {
|
|
if pkt, ok := asm.Packets[i]; ok {
|
|
payload = append(payload, pkt...)
|
|
}
|
|
}
|
|
|
|
// Size validation
|
|
if fi.PayloadSize > 0 && len(payload) != int(fi.PayloadSize) {
|
|
return
|
|
}
|
|
|
|
if len(payload) == 0 {
|
|
return
|
|
}
|
|
|
|
// Calculate RTP timestamp (90kHz for video) using relative timestamps
|
|
absoluteTS := uint64(fi.Timestamp)*1000000 + uint64(fi.TimestampUS)
|
|
if h.baseTS == 0 {
|
|
h.baseTS = absoluteTS
|
|
}
|
|
relativeUS := absoluteTS - h.baseTS
|
|
const clockRate uint64 = 90000
|
|
rtpTS := uint32(relativeUS * clockRate / 1000000)
|
|
|
|
pkt := &Packet{
|
|
Channel: channel,
|
|
Payload: payload,
|
|
Codec: fi.CodecID,
|
|
Timestamp: rtpTS,
|
|
IsKeyframe: fi.IsKeyframe(),
|
|
FrameNo: fi.FrameNo,
|
|
}
|
|
|
|
if h.verbose {
|
|
frameType := "P"
|
|
if fi.IsKeyframe() {
|
|
frameType = "I"
|
|
}
|
|
fmt.Printf("[VIDEO] #%d %s %s size=%d rtp=%d\n",
|
|
fi.FrameNo, CodecName(fi.CodecID), frameType, len(payload), rtpTS)
|
|
}
|
|
|
|
h.queue(pkt)
|
|
}
|
|
|
|
func (h *FrameHandler) handleAudio(payload []byte, fi *FrameInfo) {
|
|
if len(payload) == 0 || fi == nil {
|
|
return
|
|
}
|
|
|
|
var sampleRate uint32
|
|
var channels uint8
|
|
|
|
switch fi.CodecID {
|
|
case AudioCodecAACRaw, AudioCodecAACADTS, AudioCodecAACLATM, AudioCodecAACWyze:
|
|
sampleRate, channels = ParseAudioParams(payload, fi)
|
|
default:
|
|
sampleRate = fi.SampleRate()
|
|
channels = fi.Channels()
|
|
}
|
|
|
|
// Calculate RTP timestamp using relative timestamps (shared baseTS for A/V sync)
|
|
absoluteTS := uint64(fi.Timestamp)*1000000 + uint64(fi.TimestampUS)
|
|
if h.baseTS == 0 {
|
|
h.baseTS = absoluteTS
|
|
}
|
|
relativeUS := absoluteTS - h.baseTS
|
|
clockRate := uint64(sampleRate)
|
|
rtpTS := uint32(relativeUS * clockRate / 1000000)
|
|
|
|
pkt := &Packet{
|
|
Channel: ChannelAudio,
|
|
Payload: payload,
|
|
Codec: fi.CodecID,
|
|
Timestamp: rtpTS,
|
|
SampleRate: sampleRate,
|
|
Channels: channels,
|
|
FrameNo: fi.FrameNo,
|
|
}
|
|
|
|
if h.verbose {
|
|
fmt.Printf("[AUDIO] #%d %s size=%d rate=%d ch=%d rtp=%d\n",
|
|
fi.FrameNo, AudioCodecName(fi.CodecID), len(payload), sampleRate, channels, rtpTS)
|
|
}
|
|
|
|
h.queue(pkt)
|
|
}
|
|
|
|
func (h *FrameHandler) queue(pkt *Packet) {
|
|
select {
|
|
case h.output <- pkt:
|
|
default:
|
|
// Queue full - drop oldest
|
|
select {
|
|
case <-h.output:
|
|
default:
|
|
}
|
|
h.output <- pkt
|
|
}
|
|
}
|
|
|
|
func (h *FrameHandler) logWireHeader(data []byte, hdr *PacketHeader) {
|
|
fmt.Printf("[WIRE] ch=0x%02x type=0x%02x len=%d pkt=%d/%d frame=%d\n",
|
|
hdr.Channel, hdr.FrameType, len(data), hdr.PktIdx, hdr.PktTotal, hdr.FrameNo)
|
|
fmt.Printf(" RAW[0..35]: ")
|
|
for i := 0; i < 36 && i < len(data); i++ {
|
|
fmt.Printf("%02x ", data[i])
|
|
}
|
|
fmt.Printf("\n")
|
|
}
|
|
|
|
func (h *FrameHandler) logAVPacket(channel, frameType byte, payload []byte, fi *FrameInfo) {
|
|
fmt.Printf("[AV] ch=0x%02x type=0x%02x len=%d", channel, frameType, len(payload))
|
|
if fi != nil {
|
|
fmt.Printf(" fi={codec=0x%04x flags=0x%02x ts=%d}", fi.CodecID, fi.Flags, fi.Timestamp)
|
|
}
|
|
fmt.Printf("\n")
|
|
}
|