155 lines
4.6 KiB
Go
155 lines
4.6 KiB
Go
package nativecapture
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/TNTcraftHIM/Piik/internal/media/encoded"
|
|
)
|
|
|
|
const (
|
|
envelopeHeaderBytes = 32
|
|
maxMediaBytes = encoded.MaxAccessUnitBytes
|
|
maxStatusBytes = 4 * 1024
|
|
maxControlBytes = 64
|
|
)
|
|
|
|
type FrameKind uint8
|
|
|
|
const (
|
|
FramePCM FrameKind = 1
|
|
FrameH264 FrameKind = 2
|
|
FrameStatus FrameKind = 3
|
|
FrameVP8 FrameKind = 4
|
|
FrameBegin FrameKind = 5
|
|
FrameLayerUnavailable FrameKind = 6
|
|
FrameControl FrameKind = 7
|
|
)
|
|
|
|
type Frame struct {
|
|
Kind FrameKind
|
|
// KeyFrame denotes independent recovery, including H264 SPS/PPS when needed.
|
|
KeyFrame bool
|
|
Layer int
|
|
Width uint32
|
|
Height uint32
|
|
Timestamp time.Duration
|
|
Duration time.Duration
|
|
Data []byte
|
|
}
|
|
|
|
func readFrame(input io.Reader) (Frame, error) {
|
|
var header [envelopeHeaderBytes]byte
|
|
if _, err := io.ReadFull(input, header[:]); err != nil {
|
|
return Frame{}, err
|
|
}
|
|
frame, size, err := parseFrameHeader(header[:])
|
|
if err != nil {
|
|
return Frame{}, err
|
|
}
|
|
frame.Data = make([]byte, size)
|
|
if _, err = io.ReadFull(input, frame.Data); err != nil {
|
|
return Frame{}, err
|
|
}
|
|
if err = validateFrameData(frame); err != nil {
|
|
return Frame{}, err
|
|
}
|
|
return frame, nil
|
|
}
|
|
|
|
func parseFrameHeader(header []byte) (Frame, uint32, error) {
|
|
kind := FrameKind(header[5])
|
|
size := binary.BigEndian.Uint32(header[28:32])
|
|
maximum := uint32(maxMediaBytes)
|
|
if kind == FrameStatus || kind == FrameLayerUnavailable {
|
|
maximum = maxStatusBytes
|
|
} else if kind == FrameControl {
|
|
maximum = maxControlBytes
|
|
}
|
|
if string(header[:4]) != "SMED" || header[4] != 2 ||
|
|
kind < FramePCM || kind > FrameControl || size > maximum ||
|
|
(kind == FrameBegin) != (size == 0) {
|
|
return Frame{}, 0, errors.New("native capture frame header is invalid")
|
|
}
|
|
timestamp100ns := binary.BigEndian.Uint64(header[8:16])
|
|
duration100ns := binary.BigEndian.Uint64(header[16:24])
|
|
width := uint32(binary.BigEndian.Uint16(header[24:26]))
|
|
height := uint32(binary.BigEndian.Uint16(header[26:28]))
|
|
layer := int(header[7])
|
|
isVideo := kind == FrameH264 || kind == FrameVP8
|
|
isTimed := isVideo || kind == FramePCM || kind == FrameBegin
|
|
const maxTime100ns = uint64(1<<63-1) / 100
|
|
if timestamp100ns > maxTime100ns || duration100ns > maxTime100ns ||
|
|
(isTimed && duration100ns == 0) || (!isTimed && (timestamp100ns != 0 || duration100ns != 0)) ||
|
|
(isVideo && (header[6] > 1 || layer >= maxOutputs || width < 2 || width > 2560 ||
|
|
height < 2 || height > 1440 || width%2 != 0 || height%2 != 0)) ||
|
|
(!isVideo && (header[6] != 0 || width != 0 || height != 0)) ||
|
|
(kind == FrameLayerUnavailable && layer >= maxOutputs) ||
|
|
(!isVideo && kind != FrameLayerUnavailable && layer != 0) {
|
|
return Frame{}, 0, errors.New("native capture media frame is invalid")
|
|
}
|
|
return Frame{
|
|
Kind: kind,
|
|
KeyFrame: isVideo && header[6] == 1,
|
|
Layer: layer,
|
|
Width: width,
|
|
Height: height,
|
|
Timestamp: time.Duration(timestamp100ns) * 100 * time.Nanosecond,
|
|
Duration: time.Duration(duration100ns) * 100 * time.Nanosecond,
|
|
}, size, nil
|
|
}
|
|
|
|
func validateFrameData(frame Frame) error {
|
|
if frame.Kind == FrameLayerUnavailable && !utf8.Valid(frame.Data) {
|
|
return errors.New("native unavailable output diagnostic is invalid")
|
|
}
|
|
if frame.Kind == FrameControl {
|
|
for _, value := range frame.Data {
|
|
if value < 32 || value > 126 {
|
|
return errors.New("native control payload is not ASCII")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeFrame(output io.Writer, frame Frame) error {
|
|
if frame.Timestamp < 0 || frame.Duration < 0 || frame.Layer < 0 || frame.Layer > 255 ||
|
|
frame.Width > 65_535 || frame.Height > 65_535 || len(frame.Data) > maxMediaBytes {
|
|
return errors.New("native capture frame is outside its envelope")
|
|
}
|
|
var header [envelopeHeaderBytes]byte
|
|
copy(header[:], "SMED")
|
|
header[4], header[5], header[7] = 2, byte(frame.Kind), byte(frame.Layer)
|
|
if frame.KeyFrame {
|
|
header[6] = 1
|
|
}
|
|
binary.BigEndian.PutUint64(header[8:16], uint64(frame.Timestamp/(100*time.Nanosecond)))
|
|
binary.BigEndian.PutUint64(header[16:24], uint64(frame.Duration/(100*time.Nanosecond)))
|
|
binary.BigEndian.PutUint16(header[24:26], uint16(frame.Width))
|
|
binary.BigEndian.PutUint16(header[26:28], uint16(frame.Height))
|
|
binary.BigEndian.PutUint32(header[28:32], uint32(len(frame.Data)))
|
|
if _, _, err := parseFrameHeader(header[:]); err != nil {
|
|
return err
|
|
}
|
|
if err := validateFrameData(frame); err != nil {
|
|
return err
|
|
}
|
|
for _, data := range [][]byte{header[:], frame.Data} {
|
|
if len(data) == 0 {
|
|
continue
|
|
}
|
|
n, err := output.Write(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n != len(data) {
|
|
return io.ErrShortWrite
|
|
}
|
|
}
|
|
return nil
|
|
}
|