feat(spa): add video pod builders and format parser
- commit
- c3499e28cde9cc10c0fa530a77094fc8d39785d5
- author
- brice · · 2026-10-14 18:59 UTC
- parent
- fc13113
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +662 -0
M spa/constants.go +83 -0
| @@ -16,3 +16,86 @@ const ( | |||
| 16 | 16 | DirectionInput = 0 | |
| 17 | 17 | DirectionOutput = 1 | |
| 18 | 18 | ) | |
| 19 | + | ||
| 20 | + | // SPA pod types (spa/utils/type.h). | |
| 21 | + | const ( | |
| 22 | + | TypeId = 3 | |
| 23 | + | TypeInt = 4 | |
| 24 | + | TypeRectangle = 10 | |
| 25 | + | TypeFraction = 11 | |
| 26 | + | TypeObject = 15 | |
| 27 | + | TypeChoice = 19 | |
| 28 | + | ||
| 29 | + | TypeObjectFormat = 0x40003 // SPA_TYPE_OBJECT_Format | |
| 30 | + | TypeObjectParamBuffers = 0x40004 // SPA_TYPE_OBJECT_ParamBuffers | |
| 31 | + | TypeObjectParamMeta = 0x40005 // SPA_TYPE_OBJECT_ParamMeta | |
| 32 | + | ) | |
| 33 | + | ||
| 34 | + | // SPA choice types (spa/utils/defs.h). | |
| 35 | + | const ( | |
| 36 | + | ChoiceNone = 0 | |
| 37 | + | ChoiceRange = 1 | |
| 38 | + | ChoiceEnum = 3 | |
| 39 | + | ChoiceFlags = 4 | |
| 40 | + | ) | |
| 41 | + | ||
| 42 | + | // SPA param ids (spa/param/param.h). | |
| 43 | + | const ( | |
| 44 | + | ParamEnumFormat = 3 | |
| 45 | + | ParamFormat = 4 | |
| 46 | + | ParamBuffers = 5 | |
| 47 | + | ParamMeta = 6 | |
| 48 | + | ) | |
| 49 | + | ||
| 50 | + | // SPA format property keys (spa/param/format.h). | |
| 51 | + | const ( | |
| 52 | + | FormatKeyMediaType = 1 | |
| 53 | + | FormatKeyMediaSubtype = 2 | |
| 54 | + | FormatKeyVideoFormat = 0x20001 | |
| 55 | + | FormatKeyVideoModifier = 0x20002 | |
| 56 | + | FormatKeyVideoSize = 0x20003 | |
| 57 | + | FormatKeyVideoFramerate = 0x20004 | |
| 58 | + | FormatKeyVideoMaxRate = 0x20005 | |
| 59 | + | ) | |
| 60 | + | ||
| 61 | + | // SPA media subtypes (spa/param/format.h). | |
| 62 | + | const MediaSubtypeRaw = 1 | |
| 63 | + | ||
| 64 | + | // SPA buffers param keys (spa/param/buffers.h). | |
| 65 | + | const ( | |
| 66 | + | BuffersKeyBuffers = 1 | |
| 67 | + | BuffersKeyBlocks = 2 | |
| 68 | + | BuffersKeySize = 3 | |
| 69 | + | BuffersKeyStride = 4 | |
| 70 | + | BuffersKeyAlign = 5 | |
| 71 | + | BuffersKeyDataType = 6 | |
| 72 | + | ) | |
| 73 | + | ||
| 74 | + | // SPA meta param keys (spa/param/buffers.h) and meta types (spa/buffer/meta.h). | |
| 75 | + | const ( | |
| 76 | + | MetaKeyType = 1 | |
| 77 | + | MetaKeySize = 2 | |
| 78 | + | ||
| 79 | + | MetaTypeHeader = 1 // SPA_META_Header | |
| 80 | + | MetaHeaderSize = 32 // sizeof(struct spa_meta_header) | |
| 81 | + | ) | |
| 82 | + | ||
| 83 | + | // SPA data types (spa/buffer/buffer.h) and flags. | |
| 84 | + | const ( | |
| 85 | + | DataMemFd = 2 | |
| 86 | + | DataDmaBuf = 3 | |
| 87 | + | ||
| 88 | + | DataFlagReadWrite = 3 // SPA_DATA_FLAG_READWRITE | |
| 89 | + | ||
| 90 | + | ChunkFlagCorrupted = 1 // SPA_CHUNK_FLAG_CORRUPTED | |
| 91 | + | MetaHeaderFlagCorrupted = 2 // SPA_META_HEADER_FLAG_CORRUPTED | |
| 92 | + | IDInvalid = 0xffffffff // SPA_ID_INVALID | |
| 93 | + | ) | |
| 94 | + | ||
| 95 | + | // SPA raw video formats (spa/param/video/raw.h). | |
| 96 | + | type VideoFormat uint32 | |
| 97 | + | ||
| 98 | + | const ( | |
| 99 | + | VideoFormatBGRx VideoFormat = 8 | |
| 100 | + | VideoFormatBGRA VideoFormat = 12 | |
| 101 | + | ) | |
| @@ -16,3 +16,86 @@ const ( | |||||
| 16 | DirectionInput = 0 | 16 | DirectionInput = 0 | ||
| 17 | DirectionOutput = 1 | 17 | DirectionOutput = 1 | ||
| 18 | ) | 18 | ) | ||
| 19 | + | ||||
| 20 | + | // SPA pod types (spa/utils/type.h). | |||
| 21 | + | const ( | |||
| 22 | + | TypeId = 3 | |||
| 23 | + | TypeInt = 4 | |||
| 24 | + | TypeRectangle = 10 | |||
| 25 | + | TypeFraction = 11 | |||
| 26 | + | TypeObject = 15 | |||
| 27 | + | TypeChoice = 19 | |||
| 28 | + | ||||
| 29 | + | TypeObjectFormat = 0x40003 // SPA_TYPE_OBJECT_Format | |||
| 30 | + | TypeObjectParamBuffers = 0x40004 // SPA_TYPE_OBJECT_ParamBuffers | |||
| 31 | + | TypeObjectParamMeta = 0x40005 // SPA_TYPE_OBJECT_ParamMeta | |||
| 32 | + | ) | |||
| 33 | + | ||||
| 34 | + | // SPA choice types (spa/utils/defs.h). | |||
| 35 | + | const ( | |||
| 36 | + | ChoiceNone = 0 | |||
| 37 | + | ChoiceRange = 1 | |||
| 38 | + | ChoiceEnum = 3 | |||
| 39 | + | ChoiceFlags = 4 | |||
| 40 | + | ) | |||
| 41 | + | ||||
| 42 | + | // SPA param ids (spa/param/param.h). | |||
| 43 | + | const ( | |||
| 44 | + | ParamEnumFormat = 3 | |||
| 45 | + | ParamFormat = 4 | |||
| 46 | + | ParamBuffers = 5 | |||
| 47 | + | ParamMeta = 6 | |||
| 48 | + | ) | |||
| 49 | + | ||||
| 50 | + | // SPA format property keys (spa/param/format.h). | |||
| 51 | + | const ( | |||
| 52 | + | FormatKeyMediaType = 1 | |||
| 53 | + | FormatKeyMediaSubtype = 2 | |||
| 54 | + | FormatKeyVideoFormat = 0x20001 | |||
| 55 | + | FormatKeyVideoModifier = 0x20002 | |||
| 56 | + | FormatKeyVideoSize = 0x20003 | |||
| 57 | + | FormatKeyVideoFramerate = 0x20004 | |||
| 58 | + | FormatKeyVideoMaxRate = 0x20005 | |||
| 59 | + | ) | |||
| 60 | + | ||||
| 61 | + | // SPA media subtypes (spa/param/format.h). | |||
| 62 | + | const MediaSubtypeRaw = 1 | |||
| 63 | + | ||||
| 64 | + | // SPA buffers param keys (spa/param/buffers.h). | |||
| 65 | + | const ( | |||
| 66 | + | BuffersKeyBuffers = 1 | |||
| 67 | + | BuffersKeyBlocks = 2 | |||
| 68 | + | BuffersKeySize = 3 | |||
| 69 | + | BuffersKeyStride = 4 | |||
| 70 | + | BuffersKeyAlign = 5 | |||
| 71 | + | BuffersKeyDataType = 6 | |||
| 72 | + | ) | |||
| 73 | + | ||||
| 74 | + | // SPA meta param keys (spa/param/buffers.h) and meta types (spa/buffer/meta.h). | |||
| 75 | + | const ( | |||
| 76 | + | MetaKeyType = 1 | |||
| 77 | + | MetaKeySize = 2 | |||
| 78 | + | ||||
| 79 | + | MetaTypeHeader = 1 // SPA_META_Header | |||
| 80 | + | MetaHeaderSize = 32 // sizeof(struct spa_meta_header) | |||
| 81 | + | ) | |||
| 82 | + | ||||
| 83 | + | // SPA data types (spa/buffer/buffer.h) and flags. | |||
| 84 | + | const ( | |||
| 85 | + | DataMemFd = 2 | |||
| 86 | + | DataDmaBuf = 3 | |||
| 87 | + | ||||
| 88 | + | DataFlagReadWrite = 3 // SPA_DATA_FLAG_READWRITE | |||
| 89 | + | ||||
| 90 | + | ChunkFlagCorrupted = 1 // SPA_CHUNK_FLAG_CORRUPTED | |||
| 91 | + | MetaHeaderFlagCorrupted = 2 // SPA_META_HEADER_FLAG_CORRUPTED | |||
| 92 | + | IDInvalid = 0xffffffff // SPA_ID_INVALID | |||
| 93 | + | ) | |||
| 94 | + | ||||
| 95 | + | // SPA raw video formats (spa/param/video/raw.h). | |||
| 96 | + | type VideoFormat uint32 | |||
| 97 | + | ||||
| 98 | + | const ( | |||
| 99 | + | VideoFormatBGRx VideoFormat = 8 | |||
| 100 | + | VideoFormatBGRA VideoFormat = 12 | |||
| 101 | + | ) | |||
A spa/pod.go +360 -0
| @@ -0,0 +1,360 @@ | |||
| 1 | + | package spa | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "errors" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // Errors returned by the pod builder. | |
| 9 | + | var ( | |
| 10 | + | // ErrPodFull is returned when the destination buffer is too small. | |
| 11 | + | ErrPodFull = errors.New("spa: pod buffer full") | |
| 12 | + | // ErrPodState is returned for unbalanced Object/End or Prop outside an object. | |
| 13 | + | ErrPodState = errors.New("spa: invalid pod builder state") | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | const podMaxDepth = 4 | |
| 17 | + | ||
| 18 | + | // Fraction is a SPA fraction (num/denom). | |
| 19 | + | type Fraction struct{ Num, Denom uint32 } | |
| 20 | + | ||
| 21 | + | // PodBuilder encodes SPA pods little-endian into a caller-owned buffer. It | |
| 22 | + | // never allocates and never grows past len(buf): when the buffer is full the | |
| 23 | + | // first error is kept and returned by Bytes. The zero value is unusable; use | |
| 24 | + | // NewPodBuilder. | |
| 25 | + | type PodBuilder struct { | |
| 26 | + | buf []byte | |
| 27 | + | n int | |
| 28 | + | err error | |
| 29 | + | depth int | |
| 30 | + | stack [podMaxDepth]int // offset of each open object header | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // NewPodBuilder returns a builder writing into buf. | |
| 34 | + | func NewPodBuilder(buf []byte) PodBuilder { return PodBuilder{buf: buf} } | |
| 35 | + | ||
| 36 | + | // Bytes returns the encoded pod bytes (a prefix of the destination buffer) or | |
| 37 | + | // the first error. It fails if an Object is still open. | |
| 38 | + | func (b *PodBuilder) Bytes() ([]byte, error) { | |
| 39 | + | if b.err != nil { | |
| 40 | + | return nil, b.err | |
| 41 | + | } | |
| 42 | + | if b.depth != 0 { | |
| 43 | + | return nil, ErrPodState | |
| 44 | + | } | |
| 45 | + | return b.buf[:b.n], nil | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func (b *PodBuilder) fail(err error) { | |
| 49 | + | if b.err == nil { | |
| 50 | + | b.err = err | |
| 51 | + | } | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // room reports whether n more bytes fit, recording ErrPodFull otherwise. | |
| 55 | + | func (b *PodBuilder) room(n int) bool { | |
| 56 | + | if b.err != nil { | |
| 57 | + | return false | |
| 58 | + | } | |
| 59 | + | if n < 0 || b.n+n > len(b.buf) { | |
| 60 | + | b.fail(ErrPodFull) | |
| 61 | + | return false | |
| 62 | + | } | |
| 63 | + | return true | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func (b *PodBuilder) u32(v uint32) { | |
| 67 | + | binary.LittleEndian.PutUint32(b.buf[b.n:], v) | |
| 68 | + | b.n += 4 | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // pad8 zero-pads the write offset to a multiple of 8 bytes. | |
| 72 | + | func (b *PodBuilder) pad8() { | |
| 73 | + | for b.n&7 != 0 { | |
| 74 | + | b.buf[b.n] = 0 | |
| 75 | + | b.n++ | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // Object opens an object pod. Close it with End. | |
| 80 | + | func (b *PodBuilder) Object(objType, id uint32) { | |
| 81 | + | if !b.room(16) { | |
| 82 | + | return | |
| 83 | + | } | |
| 84 | + | if b.depth == podMaxDepth { | |
| 85 | + | b.fail(ErrPodState) | |
| 86 | + | return | |
| 87 | + | } | |
| 88 | + | b.stack[b.depth] = b.n | |
| 89 | + | b.depth++ | |
| 90 | + | b.u32(0) // size, patched by End | |
| 91 | + | b.u32(TypeObject) | |
| 92 | + | b.u32(objType) | |
| 93 | + | b.u32(id) | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // Prop starts an object property with the given key; the next value written | |
| 97 | + | // is the property value. | |
| 98 | + | func (b *PodBuilder) Prop(key uint32) { | |
| 99 | + | if b.err == nil && b.depth == 0 { | |
| 100 | + | b.fail(ErrPodState) | |
| 101 | + | return | |
| 102 | + | } | |
| 103 | + | if !b.room(8) { | |
| 104 | + | return | |
| 105 | + | } | |
| 106 | + | b.u32(key) | |
| 107 | + | b.u32(0) // flags | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | // End closes the innermost open object. | |
| 111 | + | func (b *PodBuilder) End() { | |
| 112 | + | if b.err != nil { | |
| 113 | + | return | |
| 114 | + | } | |
| 115 | + | if b.depth == 0 { | |
| 116 | + | b.fail(ErrPodState) | |
| 117 | + | return | |
| 118 | + | } | |
| 119 | + | b.depth-- | |
| 120 | + | start := b.stack[b.depth] | |
| 121 | + | binary.LittleEndian.PutUint32(b.buf[start:], uint32(b.n-start-8)) | |
| 122 | + | // Padding was already room-checked by the last value; objects always | |
| 123 | + | // end on an 8-byte boundary because every value is padded. | |
| 124 | + | b.pad8() | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | func (b *PodBuilder) scalar(typ uint32, size uint32, a, c uint32) { | |
| 128 | + | // size is the body size (4 or 8); the pod is padded to 16 bytes. | |
| 129 | + | if !b.room(16) { | |
| 130 | + | return | |
| 131 | + | } | |
| 132 | + | b.u32(size) | |
| 133 | + | b.u32(typ) | |
| 134 | + | b.u32(a) | |
| 135 | + | b.u32(c) | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // Id appends an Id pod. | |
| 139 | + | func (b *PodBuilder) Id(v uint32) { b.scalar(TypeId, 4, v, 0) } | |
| 140 | + | ||
| 141 | + | // Int appends an Int pod. | |
| 142 | + | func (b *PodBuilder) Int(v int32) { b.scalar(TypeInt, 4, uint32(v), 0) } | |
| 143 | + | ||
| 144 | + | // Rectangle appends a Rectangle pod. | |
| 145 | + | func (b *PodBuilder) Rectangle(w, h uint32) { b.scalar(TypeRectangle, 8, w, h) } | |
| 146 | + | ||
| 147 | + | // Fraction appends a Fraction pod. | |
| 148 | + | func (b *PodBuilder) Fraction(num, denom uint32) { b.scalar(TypeFraction, 8, num, denom) } | |
| 149 | + | ||
| 150 | + | // choiceBegin writes the choice header; it returns the header offset. | |
| 151 | + | func (b *PodBuilder) choiceBegin(choiceType, childSize, childType uint32, nvals int) (int, bool) { | |
| 152 | + | if !b.room(24 + nvals*int(childSize) + 8) { | |
| 153 | + | return 0, false | |
| 154 | + | } | |
| 155 | + | start := b.n | |
| 156 | + | b.u32(0) // size, patched in choiceEnd | |
| 157 | + | b.u32(TypeChoice) | |
| 158 | + | b.u32(choiceType) | |
| 159 | + | b.u32(0) // flags | |
| 160 | + | b.u32(childSize) | |
| 161 | + | b.u32(childType) | |
| 162 | + | return start, true | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | func (b *PodBuilder) choiceEnd(start int) { | |
| 166 | + | binary.LittleEndian.PutUint32(b.buf[start:], uint32(b.n-start-8)) | |
| 167 | + | b.pad8() | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | // ChoiceEnumId appends an enum choice of Ids. vals is written verbatim: the | |
| 171 | + | // first value is the default and the remaining values are the alternatives. | |
| 172 | + | func (b *PodBuilder) ChoiceEnumId(vals ...uint32) { | |
| 173 | + | if len(vals) == 0 { | |
| 174 | + | b.fail(ErrPodState) | |
| 175 | + | return | |
| 176 | + | } | |
| 177 | + | start, ok := b.choiceBegin(ChoiceEnum, 4, TypeId, len(vals)) | |
| 178 | + | if !ok { | |
| 179 | + | return | |
| 180 | + | } | |
| 181 | + | for _, v := range vals { | |
| 182 | + | b.u32(v) | |
| 183 | + | } | |
| 184 | + | b.choiceEnd(start) | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | // ChoiceRangeInt appends a range choice of Ints. | |
| 188 | + | func (b *PodBuilder) ChoiceRangeInt(def, min, max int32) { | |
| 189 | + | start, ok := b.choiceBegin(ChoiceRange, 4, TypeInt, 3) | |
| 190 | + | if !ok { | |
| 191 | + | return | |
| 192 | + | } | |
| 193 | + | b.u32(uint32(def)) | |
| 194 | + | b.u32(uint32(min)) | |
| 195 | + | b.u32(uint32(max)) | |
| 196 | + | b.choiceEnd(start) | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | // ChoiceRangeFraction appends a range choice of Fractions. | |
| 200 | + | func (b *PodBuilder) ChoiceRangeFraction(def, min, max Fraction) { | |
| 201 | + | start, ok := b.choiceBegin(ChoiceRange, 8, TypeFraction, 3) | |
| 202 | + | if !ok { | |
| 203 | + | return | |
| 204 | + | } | |
| 205 | + | for _, f := range [3]Fraction{def, min, max} { | |
| 206 | + | b.u32(f.Num) | |
| 207 | + | b.u32(f.Denom) | |
| 208 | + | } | |
| 209 | + | b.choiceEnd(start) | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | // ChoiceFlagsInt appends a flags choice of Ints with a single flags value. | |
| 213 | + | func (b *PodBuilder) ChoiceFlagsInt(flags uint32) { | |
| 214 | + | start, ok := b.choiceBegin(ChoiceFlags, 4, TypeInt, 1) | |
| 215 | + | if !ok { | |
| 216 | + | return | |
| 217 | + | } | |
| 218 | + | b.u32(flags) | |
| 219 | + | b.choiceEnd(start) | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | // VideoEnumFormat encodes an EnumFormat param for raw video of the given | |
| 223 | + | // size, maximum frame rate and formats (the first format is the default). | |
| 224 | + | // Modifiers are omitted (shared memory only). | |
| 225 | + | func VideoEnumFormat(dst []byte, w, h, maxFPS int, formats ...VideoFormat) ([]byte, error) { | |
| 226 | + | if len(formats) == 0 || w <= 0 || h <= 0 || maxFPS <= 0 { | |
| 227 | + | return nil, ErrPodState | |
| 228 | + | } | |
| 229 | + | b := NewPodBuilder(dst) | |
| 230 | + | b.Object(TypeObjectFormat, ParamEnumFormat) | |
| 231 | + | b.Prop(FormatKeyMediaType) | |
| 232 | + | b.Id(MediaTypeVideo) | |
| 233 | + | b.Prop(FormatKeyMediaSubtype) | |
| 234 | + | b.Id(MediaSubtypeRaw) | |
| 235 | + | b.Prop(FormatKeyVideoFormat) | |
| 236 | + | if len(formats) == 1 { | |
| 237 | + | b.Id(uint32(formats[0])) | |
| 238 | + | } else { | |
| 239 | + | b.podEnumFormats(formats) | |
| 240 | + | } | |
| 241 | + | b.Prop(FormatKeyVideoSize) | |
| 242 | + | b.Rectangle(uint32(w), uint32(h)) | |
| 243 | + | b.Prop(FormatKeyVideoFramerate) | |
| 244 | + | b.Fraction(0, 1) | |
| 245 | + | b.Prop(FormatKeyVideoMaxRate) | |
| 246 | + | b.ChoiceRangeFraction( | |
| 247 | + | Fraction{uint32(maxFPS), 1}, | |
| 248 | + | Fraction{1, 1}, | |
| 249 | + | Fraction{uint32(maxFPS), 1}, | |
| 250 | + | ) | |
| 251 | + | b.End() | |
| 252 | + | return b.Bytes() | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | // podEnumFormats writes an enum choice of video formats. As in SPA's own | |
| 256 | + | // builders, the first value is the default and is repeated in the list of | |
| 257 | + | // alternatives. | |
| 258 | + | func (b *PodBuilder) podEnumFormats(formats []VideoFormat) { | |
| 259 | + | start, ok := b.choiceBegin(ChoiceEnum, 4, TypeId, len(formats)+1) | |
| 260 | + | if !ok { | |
| 261 | + | return | |
| 262 | + | } | |
| 263 | + | b.u32(uint32(formats[0])) | |
| 264 | + | for _, f := range formats { | |
| 265 | + | b.u32(uint32(f)) | |
| 266 | + | } | |
| 267 | + | b.choiceEnd(start) | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | // VideoBuffers encodes a Buffers param: one block of size bytes with the | |
| 271 | + | // given stride, a buffer count range (minBuf..maxBuf, defBuf by default) and | |
| 272 | + | // the allowed SPA data types as a bit mask (for example 1<<DataMemFd). | |
| 273 | + | func VideoBuffers(dst []byte, size, stride, minBuf, defBuf, maxBuf int, dataTypes uint32) ([]byte, error) { | |
| 274 | + | if size <= 0 || stride <= 0 || minBuf <= 0 || defBuf < minBuf || maxBuf < defBuf { | |
| 275 | + | return nil, ErrPodState | |
| 276 | + | } | |
| 277 | + | b := NewPodBuilder(dst) | |
| 278 | + | b.Object(TypeObjectParamBuffers, ParamBuffers) | |
| 279 | + | b.Prop(BuffersKeyBuffers) | |
| 280 | + | b.ChoiceRangeInt(int32(defBuf), int32(minBuf), int32(maxBuf)) | |
| 281 | + | b.Prop(BuffersKeyBlocks) | |
| 282 | + | b.Int(1) | |
| 283 | + | b.Prop(BuffersKeySize) | |
| 284 | + | b.Int(int32(size)) | |
| 285 | + | b.Prop(BuffersKeyStride) | |
| 286 | + | b.Int(int32(stride)) | |
| 287 | + | b.Prop(BuffersKeyAlign) | |
| 288 | + | b.Int(16) | |
| 289 | + | b.Prop(BuffersKeyDataType) | |
| 290 | + | b.ChoiceFlagsInt(dataTypes) | |
| 291 | + | b.End() | |
| 292 | + | return b.Bytes() | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | // MetaHeader encodes a Meta param requesting a spa_meta_header (32 bytes). | |
| 296 | + | func MetaHeader(dst []byte) ([]byte, error) { | |
| 297 | + | b := NewPodBuilder(dst) | |
| 298 | + | b.Object(TypeObjectParamMeta, ParamMeta) | |
| 299 | + | b.Prop(MetaKeyType) | |
| 300 | + | b.Id(MetaTypeHeader) | |
| 301 | + | b.Prop(MetaKeySize) | |
| 302 | + | b.Int(MetaHeaderSize) | |
| 303 | + | b.End() | |
| 304 | + | return b.Bytes() | |
| 305 | + | } | |
| 306 | + | ||
| 307 | + | // ParseVideoFormat extracts the video format and size from a Format param | |
| 308 | + | // (an Object pod). It does not allocate. ok is false for malformed, | |
| 309 | + | // truncated or non-raw-video pods. | |
| 310 | + | func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) { | |
| 311 | + | if len(pod) < 16 { | |
| 312 | + | return 0, 0, 0, false | |
| 313 | + | } | |
| 314 | + | le := binary.LittleEndian | |
| 315 | + | size := int(le.Uint32(pod[0:])) | |
| 316 | + | if le.Uint32(pod[4:]) != TypeObject || size < 8 || 8+size > len(pod) { | |
| 317 | + | return 0, 0, 0, false | |
| 318 | + | } | |
| 319 | + | if le.Uint32(pod[8:]) != TypeObjectFormat { | |
| 320 | + | return 0, 0, 0, false | |
| 321 | + | } | |
| 322 | + | end := 8 + size | |
| 323 | + | off := 16 | |
| 324 | + | var haveFmt, haveSize, isVideo, isRaw bool | |
| 325 | + | for off+16 <= end { | |
| 326 | + | key := le.Uint32(pod[off:]) | |
| 327 | + | vsize := int(le.Uint32(pod[off+8:])) | |
| 328 | + | vtype := le.Uint32(pod[off+12:]) | |
| 329 | + | body := off + 16 | |
| 330 | + | if vsize < 0 || body+vsize > end { | |
| 331 | + | return 0, 0, 0, false | |
| 332 | + | } | |
| 333 | + | switch { | |
| 334 | + | case key == FormatKeyMediaType && vtype == TypeId && vsize >= 4: | |
| 335 | + | isVideo = le.Uint32(pod[body:]) == MediaTypeVideo | |
| 336 | + | case key == FormatKeyMediaSubtype && vtype == TypeId && vsize >= 4: | |
| 337 | + | isRaw = le.Uint32(pod[body:]) == MediaSubtypeRaw | |
| 338 | + | case key == FormatKeyVideoFormat && vtype == TypeId && vsize >= 4: | |
| 339 | + | format = le.Uint32(pod[body:]) | |
| 340 | + | haveFmt = true | |
| 341 | + | case key == FormatKeyVideoSize && vtype == TypeRectangle && vsize >= 8: | |
| 342 | + | uw, uh := le.Uint32(pod[body:]), le.Uint32(pod[body+4:]) | |
| 343 | + | if uw == 0 || uh == 0 || uw > 1<<20 || uh > 1<<20 { | |
| 344 | + | return 0, 0, 0, false | |
| 345 | + | } | |
| 346 | + | w, h = int(uw), int(uh) | |
| 347 | + | haveSize = true | |
| 348 | + | } | |
| 349 | + | next := body + vsize | |
| 350 | + | next = (next + 7) &^ 7 | |
| 351 | + | if next <= off { | |
| 352 | + | return 0, 0, 0, false | |
| 353 | + | } | |
| 354 | + | off = next | |
| 355 | + | } | |
| 356 | + | if !(haveFmt && haveSize && isVideo && isRaw) { | |
| 357 | + | return 0, 0, 0, false | |
| 358 | + | } | |
| 359 | + | return format, w, h, true | |
| 360 | + | } | |
| @@ -0,0 +1,360 @@ | |||||
| 1 | + | package spa | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "errors" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // Errors returned by the pod builder. | |||
| 9 | + | var ( | |||
| 10 | + | // ErrPodFull is returned when the destination buffer is too small. | |||
| 11 | + | ErrPodFull = errors.New("spa: pod buffer full") | |||
| 12 | + | // ErrPodState is returned for unbalanced Object/End or Prop outside an object. | |||
| 13 | + | ErrPodState = errors.New("spa: invalid pod builder state") | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | const podMaxDepth = 4 | |||
| 17 | + | ||||
| 18 | + | // Fraction is a SPA fraction (num/denom). | |||
| 19 | + | type Fraction struct{ Num, Denom uint32 } | |||
| 20 | + | ||||
| 21 | + | // PodBuilder encodes SPA pods little-endian into a caller-owned buffer. It | |||
| 22 | + | // never allocates and never grows past len(buf): when the buffer is full the | |||
| 23 | + | // first error is kept and returned by Bytes. The zero value is unusable; use | |||
| 24 | + | // NewPodBuilder. | |||
| 25 | + | type PodBuilder struct { | |||
| 26 | + | buf []byte | |||
| 27 | + | n int | |||
| 28 | + | err error | |||
| 29 | + | depth int | |||
| 30 | + | stack [podMaxDepth]int // offset of each open object header | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | // NewPodBuilder returns a builder writing into buf. | |||
| 34 | + | func NewPodBuilder(buf []byte) PodBuilder { return PodBuilder{buf: buf} } | |||
| 35 | + | ||||
| 36 | + | // Bytes returns the encoded pod bytes (a prefix of the destination buffer) or | |||
| 37 | + | // the first error. It fails if an Object is still open. | |||
| 38 | + | func (b *PodBuilder) Bytes() ([]byte, error) { | |||
| 39 | + | if b.err != nil { | |||
| 40 | + | return nil, b.err | |||
| 41 | + | } | |||
| 42 | + | if b.depth != 0 { | |||
| 43 | + | return nil, ErrPodState | |||
| 44 | + | } | |||
| 45 | + | return b.buf[:b.n], nil | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func (b *PodBuilder) fail(err error) { | |||
| 49 | + | if b.err == nil { | |||
| 50 | + | b.err = err | |||
| 51 | + | } | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // room reports whether n more bytes fit, recording ErrPodFull otherwise. | |||
| 55 | + | func (b *PodBuilder) room(n int) bool { | |||
| 56 | + | if b.err != nil { | |||
| 57 | + | return false | |||
| 58 | + | } | |||
| 59 | + | if n < 0 || b.n+n > len(b.buf) { | |||
| 60 | + | b.fail(ErrPodFull) | |||
| 61 | + | return false | |||
| 62 | + | } | |||
| 63 | + | return true | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | func (b *PodBuilder) u32(v uint32) { | |||
| 67 | + | binary.LittleEndian.PutUint32(b.buf[b.n:], v) | |||
| 68 | + | b.n += 4 | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // pad8 zero-pads the write offset to a multiple of 8 bytes. | |||
| 72 | + | func (b *PodBuilder) pad8() { | |||
| 73 | + | for b.n&7 != 0 { | |||
| 74 | + | b.buf[b.n] = 0 | |||
| 75 | + | b.n++ | |||
| 76 | + | } | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // Object opens an object pod. Close it with End. | |||
| 80 | + | func (b *PodBuilder) Object(objType, id uint32) { | |||
| 81 | + | if !b.room(16) { | |||
| 82 | + | return | |||
| 83 | + | } | |||
| 84 | + | if b.depth == podMaxDepth { | |||
| 85 | + | b.fail(ErrPodState) | |||
| 86 | + | return | |||
| 87 | + | } | |||
| 88 | + | b.stack[b.depth] = b.n | |||
| 89 | + | b.depth++ | |||
| 90 | + | b.u32(0) // size, patched by End | |||
| 91 | + | b.u32(TypeObject) | |||
| 92 | + | b.u32(objType) | |||
| 93 | + | b.u32(id) | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | // Prop starts an object property with the given key; the next value written | |||
| 97 | + | // is the property value. | |||
| 98 | + | func (b *PodBuilder) Prop(key uint32) { | |||
| 99 | + | if b.err == nil && b.depth == 0 { | |||
| 100 | + | b.fail(ErrPodState) | |||
| 101 | + | return | |||
| 102 | + | } | |||
| 103 | + | if !b.room(8) { | |||
| 104 | + | return | |||
| 105 | + | } | |||
| 106 | + | b.u32(key) | |||
| 107 | + | b.u32(0) // flags | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | // End closes the innermost open object. | |||
| 111 | + | func (b *PodBuilder) End() { | |||
| 112 | + | if b.err != nil { | |||
| 113 | + | return | |||
| 114 | + | } | |||
| 115 | + | if b.depth == 0 { | |||
| 116 | + | b.fail(ErrPodState) | |||
| 117 | + | return | |||
| 118 | + | } | |||
| 119 | + | b.depth-- | |||
| 120 | + | start := b.stack[b.depth] | |||
| 121 | + | binary.LittleEndian.PutUint32(b.buf[start:], uint32(b.n-start-8)) | |||
| 122 | + | // Padding was already room-checked by the last value; objects always | |||
| 123 | + | // end on an 8-byte boundary because every value is padded. | |||
| 124 | + | b.pad8() | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func (b *PodBuilder) scalar(typ uint32, size uint32, a, c uint32) { | |||
| 128 | + | // size is the body size (4 or 8); the pod is padded to 16 bytes. | |||
| 129 | + | if !b.room(16) { | |||
| 130 | + | return | |||
| 131 | + | } | |||
| 132 | + | b.u32(size) | |||
| 133 | + | b.u32(typ) | |||
| 134 | + | b.u32(a) | |||
| 135 | + | b.u32(c) | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | // Id appends an Id pod. | |||
| 139 | + | func (b *PodBuilder) Id(v uint32) { b.scalar(TypeId, 4, v, 0) } | |||
| 140 | + | ||||
| 141 | + | // Int appends an Int pod. | |||
| 142 | + | func (b *PodBuilder) Int(v int32) { b.scalar(TypeInt, 4, uint32(v), 0) } | |||
| 143 | + | ||||
| 144 | + | // Rectangle appends a Rectangle pod. | |||
| 145 | + | func (b *PodBuilder) Rectangle(w, h uint32) { b.scalar(TypeRectangle, 8, w, h) } | |||
| 146 | + | ||||
| 147 | + | // Fraction appends a Fraction pod. | |||
| 148 | + | func (b *PodBuilder) Fraction(num, denom uint32) { b.scalar(TypeFraction, 8, num, denom) } | |||
| 149 | + | ||||
| 150 | + | // choiceBegin writes the choice header; it returns the header offset. | |||
| 151 | + | func (b *PodBuilder) choiceBegin(choiceType, childSize, childType uint32, nvals int) (int, bool) { | |||
| 152 | + | if !b.room(24 + nvals*int(childSize) + 8) { | |||
| 153 | + | return 0, false | |||
| 154 | + | } | |||
| 155 | + | start := b.n | |||
| 156 | + | b.u32(0) // size, patched in choiceEnd | |||
| 157 | + | b.u32(TypeChoice) | |||
| 158 | + | b.u32(choiceType) | |||
| 159 | + | b.u32(0) // flags | |||
| 160 | + | b.u32(childSize) | |||
| 161 | + | b.u32(childType) | |||
| 162 | + | return start, true | |||
| 163 | + | } | |||
| 164 | + | ||||
| 165 | + | func (b *PodBuilder) choiceEnd(start int) { | |||
| 166 | + | binary.LittleEndian.PutUint32(b.buf[start:], uint32(b.n-start-8)) | |||
| 167 | + | b.pad8() | |||
| 168 | + | } | |||
| 169 | + | ||||
| 170 | + | // ChoiceEnumId appends an enum choice of Ids. vals is written verbatim: the | |||
| 171 | + | // first value is the default and the remaining values are the alternatives. | |||
| 172 | + | func (b *PodBuilder) ChoiceEnumId(vals ...uint32) { | |||
| 173 | + | if len(vals) == 0 { | |||
| 174 | + | b.fail(ErrPodState) | |||
| 175 | + | return | |||
| 176 | + | } | |||
| 177 | + | start, ok := b.choiceBegin(ChoiceEnum, 4, TypeId, len(vals)) | |||
| 178 | + | if !ok { | |||
| 179 | + | return | |||
| 180 | + | } | |||
| 181 | + | for _, v := range vals { | |||
| 182 | + | b.u32(v) | |||
| 183 | + | } | |||
| 184 | + | b.choiceEnd(start) | |||
| 185 | + | } | |||
| 186 | + | ||||
| 187 | + | // ChoiceRangeInt appends a range choice of Ints. | |||
| 188 | + | func (b *PodBuilder) ChoiceRangeInt(def, min, max int32) { | |||
| 189 | + | start, ok := b.choiceBegin(ChoiceRange, 4, TypeInt, 3) | |||
| 190 | + | if !ok { | |||
| 191 | + | return | |||
| 192 | + | } | |||
| 193 | + | b.u32(uint32(def)) | |||
| 194 | + | b.u32(uint32(min)) | |||
| 195 | + | b.u32(uint32(max)) | |||
| 196 | + | b.choiceEnd(start) | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | // ChoiceRangeFraction appends a range choice of Fractions. | |||
| 200 | + | func (b *PodBuilder) ChoiceRangeFraction(def, min, max Fraction) { | |||
| 201 | + | start, ok := b.choiceBegin(ChoiceRange, 8, TypeFraction, 3) | |||
| 202 | + | if !ok { | |||
| 203 | + | return | |||
| 204 | + | } | |||
| 205 | + | for _, f := range [3]Fraction{def, min, max} { | |||
| 206 | + | b.u32(f.Num) | |||
| 207 | + | b.u32(f.Denom) | |||
| 208 | + | } | |||
| 209 | + | b.choiceEnd(start) | |||
| 210 | + | } | |||
| 211 | + | ||||
| 212 | + | // ChoiceFlagsInt appends a flags choice of Ints with a single flags value. | |||
| 213 | + | func (b *PodBuilder) ChoiceFlagsInt(flags uint32) { | |||
| 214 | + | start, ok := b.choiceBegin(ChoiceFlags, 4, TypeInt, 1) | |||
| 215 | + | if !ok { | |||
| 216 | + | return | |||
| 217 | + | } | |||
| 218 | + | b.u32(flags) | |||
| 219 | + | b.choiceEnd(start) | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | // VideoEnumFormat encodes an EnumFormat param for raw video of the given | |||
| 223 | + | // size, maximum frame rate and formats (the first format is the default). | |||
| 224 | + | // Modifiers are omitted (shared memory only). | |||
| 225 | + | func VideoEnumFormat(dst []byte, w, h, maxFPS int, formats ...VideoFormat) ([]byte, error) { | |||
| 226 | + | if len(formats) == 0 || w <= 0 || h <= 0 || maxFPS <= 0 { | |||
| 227 | + | return nil, ErrPodState | |||
| 228 | + | } | |||
| 229 | + | b := NewPodBuilder(dst) | |||
| 230 | + | b.Object(TypeObjectFormat, ParamEnumFormat) | |||
| 231 | + | b.Prop(FormatKeyMediaType) | |||
| 232 | + | b.Id(MediaTypeVideo) | |||
| 233 | + | b.Prop(FormatKeyMediaSubtype) | |||
| 234 | + | b.Id(MediaSubtypeRaw) | |||
| 235 | + | b.Prop(FormatKeyVideoFormat) | |||
| 236 | + | if len(formats) == 1 { | |||
| 237 | + | b.Id(uint32(formats[0])) | |||
| 238 | + | } else { | |||
| 239 | + | b.podEnumFormats(formats) | |||
| 240 | + | } | |||
| 241 | + | b.Prop(FormatKeyVideoSize) | |||
| 242 | + | b.Rectangle(uint32(w), uint32(h)) | |||
| 243 | + | b.Prop(FormatKeyVideoFramerate) | |||
| 244 | + | b.Fraction(0, 1) | |||
| 245 | + | b.Prop(FormatKeyVideoMaxRate) | |||
| 246 | + | b.ChoiceRangeFraction( | |||
| 247 | + | Fraction{uint32(maxFPS), 1}, | |||
| 248 | + | Fraction{1, 1}, | |||
| 249 | + | Fraction{uint32(maxFPS), 1}, | |||
| 250 | + | ) | |||
| 251 | + | b.End() | |||
| 252 | + | return b.Bytes() | |||
| 253 | + | } | |||
| 254 | + | ||||
| 255 | + | // podEnumFormats writes an enum choice of video formats. As in SPA's own | |||
| 256 | + | // builders, the first value is the default and is repeated in the list of | |||
| 257 | + | // alternatives. | |||
| 258 | + | func (b *PodBuilder) podEnumFormats(formats []VideoFormat) { | |||
| 259 | + | start, ok := b.choiceBegin(ChoiceEnum, 4, TypeId, len(formats)+1) | |||
| 260 | + | if !ok { | |||
| 261 | + | return | |||
| 262 | + | } | |||
| 263 | + | b.u32(uint32(formats[0])) | |||
| 264 | + | for _, f := range formats { | |||
| 265 | + | b.u32(uint32(f)) | |||
| 266 | + | } | |||
| 267 | + | b.choiceEnd(start) | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | // VideoBuffers encodes a Buffers param: one block of size bytes with the | |||
| 271 | + | // given stride, a buffer count range (minBuf..maxBuf, defBuf by default) and | |||
| 272 | + | // the allowed SPA data types as a bit mask (for example 1<<DataMemFd). | |||
| 273 | + | func VideoBuffers(dst []byte, size, stride, minBuf, defBuf, maxBuf int, dataTypes uint32) ([]byte, error) { | |||
| 274 | + | if size <= 0 || stride <= 0 || minBuf <= 0 || defBuf < minBuf || maxBuf < defBuf { | |||
| 275 | + | return nil, ErrPodState | |||
| 276 | + | } | |||
| 277 | + | b := NewPodBuilder(dst) | |||
| 278 | + | b.Object(TypeObjectParamBuffers, ParamBuffers) | |||
| 279 | + | b.Prop(BuffersKeyBuffers) | |||
| 280 | + | b.ChoiceRangeInt(int32(defBuf), int32(minBuf), int32(maxBuf)) | |||
| 281 | + | b.Prop(BuffersKeyBlocks) | |||
| 282 | + | b.Int(1) | |||
| 283 | + | b.Prop(BuffersKeySize) | |||
| 284 | + | b.Int(int32(size)) | |||
| 285 | + | b.Prop(BuffersKeyStride) | |||
| 286 | + | b.Int(int32(stride)) | |||
| 287 | + | b.Prop(BuffersKeyAlign) | |||
| 288 | + | b.Int(16) | |||
| 289 | + | b.Prop(BuffersKeyDataType) | |||
| 290 | + | b.ChoiceFlagsInt(dataTypes) | |||
| 291 | + | b.End() | |||
| 292 | + | return b.Bytes() | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | // MetaHeader encodes a Meta param requesting a spa_meta_header (32 bytes). | |||
| 296 | + | func MetaHeader(dst []byte) ([]byte, error) { | |||
| 297 | + | b := NewPodBuilder(dst) | |||
| 298 | + | b.Object(TypeObjectParamMeta, ParamMeta) | |||
| 299 | + | b.Prop(MetaKeyType) | |||
| 300 | + | b.Id(MetaTypeHeader) | |||
| 301 | + | b.Prop(MetaKeySize) | |||
| 302 | + | b.Int(MetaHeaderSize) | |||
| 303 | + | b.End() | |||
| 304 | + | return b.Bytes() | |||
| 305 | + | } | |||
| 306 | + | ||||
| 307 | + | // ParseVideoFormat extracts the video format and size from a Format param | |||
| 308 | + | // (an Object pod). It does not allocate. ok is false for malformed, | |||
| 309 | + | // truncated or non-raw-video pods. | |||
| 310 | + | func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) { | |||
| 311 | + | if len(pod) < 16 { | |||
| 312 | + | return 0, 0, 0, false | |||
| 313 | + | } | |||
| 314 | + | le := binary.LittleEndian | |||
| 315 | + | size := int(le.Uint32(pod[0:])) | |||
| 316 | + | if le.Uint32(pod[4:]) != TypeObject || size < 8 || 8+size > len(pod) { | |||
| 317 | + | return 0, 0, 0, false | |||
| 318 | + | } | |||
| 319 | + | if le.Uint32(pod[8:]) != TypeObjectFormat { | |||
| 320 | + | return 0, 0, 0, false | |||
| 321 | + | } | |||
| 322 | + | end := 8 + size | |||
| 323 | + | off := 16 | |||
| 324 | + | var haveFmt, haveSize, isVideo, isRaw bool | |||
| 325 | + | for off+16 <= end { | |||
| 326 | + | key := le.Uint32(pod[off:]) | |||
| 327 | + | vsize := int(le.Uint32(pod[off+8:])) | |||
| 328 | + | vtype := le.Uint32(pod[off+12:]) | |||
| 329 | + | body := off + 16 | |||
| 330 | + | if vsize < 0 || body+vsize > end { | |||
| 331 | + | return 0, 0, 0, false | |||
| 332 | + | } | |||
| 333 | + | switch { | |||
| 334 | + | case key == FormatKeyMediaType && vtype == TypeId && vsize >= 4: | |||
| 335 | + | isVideo = le.Uint32(pod[body:]) == MediaTypeVideo | |||
| 336 | + | case key == FormatKeyMediaSubtype && vtype == TypeId && vsize >= 4: | |||
| 337 | + | isRaw = le.Uint32(pod[body:]) == MediaSubtypeRaw | |||
| 338 | + | case key == FormatKeyVideoFormat && vtype == TypeId && vsize >= 4: | |||
| 339 | + | format = le.Uint32(pod[body:]) | |||
| 340 | + | haveFmt = true | |||
| 341 | + | case key == FormatKeyVideoSize && vtype == TypeRectangle && vsize >= 8: | |||
| 342 | + | uw, uh := le.Uint32(pod[body:]), le.Uint32(pod[body+4:]) | |||
| 343 | + | if uw == 0 || uh == 0 || uw > 1<<20 || uh > 1<<20 { | |||
| 344 | + | return 0, 0, 0, false | |||
| 345 | + | } | |||
| 346 | + | w, h = int(uw), int(uh) | |||
| 347 | + | haveSize = true | |||
| 348 | + | } | |||
| 349 | + | next := body + vsize | |||
| 350 | + | next = (next + 7) &^ 7 | |||
| 351 | + | if next <= off { | |||
| 352 | + | return 0, 0, 0, false | |||
| 353 | + | } | |||
| 354 | + | off = next | |||
| 355 | + | } | |||
| 356 | + | if !(haveFmt && haveSize && isVideo && isRaw) { | |||
| 357 | + | return 0, 0, 0, false | |||
| 358 | + | } | |||
| 359 | + | return format, w, h, true | |||
| 360 | + | } | |||
A spa/pod_test.go +219 -0
| @@ -0,0 +1,219 @@ | |||
| 1 | + | package spa | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "encoding/hex" | |
| 6 | + | "testing" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // Golden vectors are the bytes produced by the SPA reference builder | |
| 10 | + | // (spa/pod/builder.h, libspa 0.2 / PipeWire 1.6.9) for the same parameters | |
| 11 | + | // via spa_pod_builder_add_object / SPA_POD_CHOICE_* on amd64. | |
| 12 | + | const ( | |
| 13 | + | goldenEnumFormatSingle = "b80000000f00000003000400030000000100000000000000040000000300000002000000000000000200000000000000040000000300000001000000000000000100020000000000040000000300000008000000000000000300020000000000080000000a00000040010000f00000000400020000000000080000000b0000000000000001000000050002000000000028000000130000000100000000000000080000000b0000003c0000000100000001000000010000003c00000001000000" | |
| 14 | + | goldenEnumFormatMulti = "d00000000f000000030004000300000001000000000000000400000003000000020000000000000002000000000000000400000003000000010000000000000001000200000000001c000000130000000300000000000000040000000300000008000000080000000c000000000000000300020000000000080000000a00000080070000380400000400020000000000080000000b0000000000000001000000050002000000000028000000130000000100000000000000080000000b000000900000000100000001000000010000009000000001000000" | |
| 15 | + | goldenBuffers = "c00000000f000000040004000500000001000000000000001c0000001300000001000000000000000400000004000000080000000200000010000000000000000200000000000000040000000400000001000000000000000300000000000000040000000400000000b004000000000004000000000000000400000004000000000500000000000005000000000000000400000004000000100000000000000006000000000000001400000013000000040000000000000004000000040000000400000000000000" | |
| 16 | + | goldenMetaHeader = "380000000f0000000500040006000000010000000000000004000000030000000100000000000000020000000000000004000000040000002000000000000000" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | func mustHex(t testing.TB, s string) []byte { | |
| 20 | + | t.Helper() | |
| 21 | + | b, err := hex.DecodeString(s) | |
| 22 | + | if err != nil { | |
| 23 | + | t.Fatal(err) | |
| 24 | + | } | |
| 25 | + | return b | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func TestVideoEnumFormatGoldenSingle(t *testing.T) { | |
| 29 | + | var dst [512]byte | |
| 30 | + | got, err := VideoEnumFormat(dst[:], 320, 240, 60, VideoFormatBGRx) | |
| 31 | + | if err != nil { | |
| 32 | + | t.Fatal(err) | |
| 33 | + | } | |
| 34 | + | if want := mustHex(t, goldenEnumFormatSingle); hex.EncodeToString(got) != hex.EncodeToString(want) { | |
| 35 | + | t.Fatalf("got %x\nwant %x", got, want) | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | func TestVideoEnumFormatGoldenMulti(t *testing.T) { | |
| 40 | + | var dst [512]byte | |
| 41 | + | got, err := VideoEnumFormat(dst[:], 1920, 1080, 144, VideoFormatBGRx, VideoFormatBGRA) | |
| 42 | + | if err != nil { | |
| 43 | + | t.Fatal(err) | |
| 44 | + | } | |
| 45 | + | if want := mustHex(t, goldenEnumFormatMulti); hex.EncodeToString(got) != hex.EncodeToString(want) { | |
| 46 | + | t.Fatalf("got %x\nwant %x", got, want) | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func TestVideoBuffersGolden(t *testing.T) { | |
| 51 | + | var dst [512]byte | |
| 52 | + | got, err := VideoBuffers(dst[:], 320*240*4, 320*4, 2, 8, 16, 1<<DataMemFd) | |
| 53 | + | if err != nil { | |
| 54 | + | t.Fatal(err) | |
| 55 | + | } | |
| 56 | + | if want := mustHex(t, goldenBuffers); hex.EncodeToString(got) != hex.EncodeToString(want) { | |
| 57 | + | t.Fatalf("got %x\nwant %x", got, want) | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | func TestMetaHeaderGolden(t *testing.T) { | |
| 62 | + | var dst [128]byte | |
| 63 | + | got, err := MetaHeader(dst[:]) | |
| 64 | + | if err != nil { | |
| 65 | + | t.Fatal(err) | |
| 66 | + | } | |
| 67 | + | if want := mustHex(t, goldenMetaHeader); hex.EncodeToString(got) != hex.EncodeToString(want) { | |
| 68 | + | t.Fatalf("got %x\nwant %x", got, want) | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | // formatPod builds a negotiated (non-choice) Format object for parser tests. | |
| 73 | + | func formatPod(t testing.TB, dst []byte, format VideoFormat, w, h uint32) []byte { | |
| 74 | + | t.Helper() | |
| 75 | + | b := NewPodBuilder(dst) | |
| 76 | + | b.Object(TypeObjectFormat, ParamFormat) | |
| 77 | + | b.Prop(FormatKeyMediaType) | |
| 78 | + | b.Id(MediaTypeVideo) | |
| 79 | + | b.Prop(FormatKeyMediaSubtype) | |
| 80 | + | b.Id(MediaSubtypeRaw) | |
| 81 | + | b.Prop(FormatKeyVideoFormat) | |
| 82 | + | b.Id(uint32(format)) | |
| 83 | + | b.Prop(FormatKeyVideoSize) | |
| 84 | + | b.Rectangle(w, h) | |
| 85 | + | b.Prop(FormatKeyVideoFramerate) | |
| 86 | + | b.Fraction(60, 1) | |
| 87 | + | b.End() | |
| 88 | + | pod, err := b.Bytes() | |
| 89 | + | if err != nil { | |
| 90 | + | t.Fatal(err) | |
| 91 | + | } | |
| 92 | + | return pod | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | func TestParseVideoFormatRoundTrip(t *testing.T) { | |
| 96 | + | var dst [256]byte | |
| 97 | + | for _, tc := range []struct { | |
| 98 | + | f VideoFormat | |
| 99 | + | w, h int | |
| 100 | + | }{{VideoFormatBGRx, 320, 240}, {VideoFormatBGRA, 3840, 2160}} { | |
| 101 | + | pod := formatPod(t, dst[:], tc.f, uint32(tc.w), uint32(tc.h)) | |
| 102 | + | f, w, h, ok := ParseVideoFormat(pod) | |
| 103 | + | if !ok || VideoFormat(f) != tc.f || w != tc.w || h != tc.h { | |
| 104 | + | t.Fatalf("parse = %d %d %d %v, want %d %d %d", f, w, h, ok, tc.f, tc.w, tc.h) | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | func TestParseVideoFormatRejectsMalformed(t *testing.T) { | |
| 110 | + | var dst [256]byte | |
| 111 | + | pod := append([]byte(nil), formatPod(t, dst[:], VideoFormatBGRx, 320, 240)...) | |
| 112 | + | ||
| 113 | + | // Every truncation must be rejected without panicking. | |
| 114 | + | for n := 0; n < len(pod); n++ { | |
| 115 | + | if _, _, _, ok := ParseVideoFormat(pod[:n]); ok { | |
| 116 | + | t.Fatalf("truncated to %d bytes accepted", n) | |
| 117 | + | } | |
| 118 | + | } | |
| 119 | + | // Non-object, wrong object type, oversized declared size. | |
| 120 | + | bad := append([]byte(nil), pod...) | |
| 121 | + | binary.LittleEndian.PutUint32(bad[4:], TypeInt) | |
| 122 | + | if _, _, _, ok := ParseVideoFormat(bad); ok { | |
| 123 | + | t.Fatal("non-object accepted") | |
| 124 | + | } | |
| 125 | + | bad = append([]byte(nil), pod...) | |
| 126 | + | binary.LittleEndian.PutUint32(bad[8:], TypeObjectParamBuffers) | |
| 127 | + | if _, _, _, ok := ParseVideoFormat(bad); ok { | |
| 128 | + | t.Fatal("wrong object type accepted") | |
| 129 | + | } | |
| 130 | + | bad = append([]byte(nil), pod...) | |
| 131 | + | binary.LittleEndian.PutUint32(bad[0:], 1<<30) | |
| 132 | + | if _, _, _, ok := ParseVideoFormat(bad); ok { | |
| 133 | + | t.Fatal("oversized pod accepted") | |
| 134 | + | } | |
| 135 | + | // Property value size overflowing the object. | |
| 136 | + | bad = append([]byte(nil), pod...) | |
| 137 | + | binary.LittleEndian.PutUint32(bad[16+8:], 1<<31) | |
| 138 | + | if _, _, _, ok := ParseVideoFormat(bad); ok { | |
| 139 | + | t.Fatal("oversized property accepted") | |
| 140 | + | } | |
| 141 | + | // Zero and absurd sizes. | |
| 142 | + | if _, _, _, ok := ParseVideoFormat(formatPod(t, dst[:], VideoFormatBGRx, 0, 240)); ok { | |
| 143 | + | t.Fatal("zero width accepted") | |
| 144 | + | } | |
| 145 | + | if _, _, _, ok := ParseVideoFormat(formatPod(t, dst[:], VideoFormatBGRx, 1<<24, 240)); ok { | |
| 146 | + | t.Fatal("huge width accepted") | |
| 147 | + | } | |
| 148 | + | // An EnumFormat with a choice for the format is not a negotiated format. | |
| 149 | + | var big [512]byte | |
| 150 | + | enum, err := VideoEnumFormat(big[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA) | |
| 151 | + | if err != nil { | |
| 152 | + | t.Fatal(err) | |
| 153 | + | } | |
| 154 | + | if _, _, _, ok := ParseVideoFormat(enum); ok { | |
| 155 | + | t.Fatal("choice format accepted") | |
| 156 | + | } | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | func TestPodBuilderErrors(t *testing.T) { | |
| 160 | + | // Buffer too small: error, no panic, for every size up to the full pod. | |
| 161 | + | var full [512]byte | |
| 162 | + | pod, err := VideoEnumFormat(full[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA) | |
| 163 | + | if err != nil { | |
| 164 | + | t.Fatal(err) | |
| 165 | + | } | |
| 166 | + | for n := 0; n < len(pod); n++ { | |
| 167 | + | small := make([]byte, n) | |
| 168 | + | if _, err := VideoEnumFormat(small, 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA); err != ErrPodFull { | |
| 169 | + | t.Fatalf("size %d: err = %v, want ErrPodFull", n, err) | |
| 170 | + | } | |
| 171 | + | } | |
| 172 | + | if _, err := VideoBuffers(make([]byte, 16), 1, 1, 2, 8, 16, 4); err != ErrPodFull { | |
| 173 | + | t.Fatalf("buffers small: %v", err) | |
| 174 | + | } | |
| 175 | + | if _, err := MetaHeader(make([]byte, 8)); err != ErrPodFull { | |
| 176 | + | t.Fatalf("meta small: %v", err) | |
| 177 | + | } | |
| 178 | + | // Invalid arguments. | |
| 179 | + | if _, err := VideoEnumFormat(full[:], 0, 240, 60, VideoFormatBGRx); err == nil { | |
| 180 | + | t.Fatal("zero width accepted") | |
| 181 | + | } | |
| 182 | + | if _, err := VideoEnumFormat(full[:], 320, 240, 60); err == nil { | |
| 183 | + | t.Fatal("no formats accepted") | |
| 184 | + | } | |
| 185 | + | if _, err := VideoBuffers(full[:], 0, 4, 2, 8, 16, 4); err == nil { | |
| 186 | + | t.Fatal("zero size accepted") | |
| 187 | + | } | |
| 188 | + | // Unbalanced builder use. | |
| 189 | + | b := NewPodBuilder(full[:]) | |
| 190 | + | b.Prop(1) | |
| 191 | + | if _, err := b.Bytes(); err != ErrPodState { | |
| 192 | + | t.Fatalf("prop outside object: %v", err) | |
| 193 | + | } | |
| 194 | + | b = NewPodBuilder(full[:]) | |
| 195 | + | b.Object(TypeObjectFormat, ParamFormat) | |
| 196 | + | if _, err := b.Bytes(); err != ErrPodState { | |
| 197 | + | t.Fatalf("open object: %v", err) | |
| 198 | + | } | |
| 199 | + | b = NewPodBuilder(full[:]) | |
| 200 | + | b.End() | |
| 201 | + | if _, err := b.Bytes(); err != ErrPodState { | |
| 202 | + | t.Fatalf("end without object: %v", err) | |
| 203 | + | } | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | func TestPodAllocations(t *testing.T) { | |
| 207 | + | var dst [512]byte | |
| 208 | + | pod := append([]byte(nil), formatPod(t, dst[:], VideoFormatBGRx, 320, 240)...) | |
| 209 | + | allocs := testing.AllocsPerRun(200, func() { | |
| 210 | + | e, _ := VideoEnumFormat(dst[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA) | |
| 211 | + | _, _ = VideoBuffers(dst[:], 307200, 1280, 2, 8, 16, 1<<DataMemFd) | |
| 212 | + | _, _ = MetaHeader(dst[:]) | |
| 213 | + | _ = e | |
| 214 | + | _, _, _, _ = ParseVideoFormat(pod) | |
| 215 | + | }) | |
| 216 | + | if allocs != 0 { | |
| 217 | + | t.Fatalf("allocs = %v, want 0", allocs) | |
| 218 | + | } | |
| 219 | + | } | |
| @@ -0,0 +1,219 @@ | |||||
| 1 | + | package spa | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "encoding/hex" | |||
| 6 | + | "testing" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // Golden vectors are the bytes produced by the SPA reference builder | |||
| 10 | + | // (spa/pod/builder.h, libspa 0.2 / PipeWire 1.6.9) for the same parameters | |||
| 11 | + | // via spa_pod_builder_add_object / SPA_POD_CHOICE_* on amd64. | |||
| 12 | + | const ( | |||
| 13 | + | goldenEnumFormatSingle = "b80000000f00000003000400030000000100000000000000040000000300000002000000000000000200000000000000040000000300000001000000000000000100020000000000040000000300000008000000000000000300020000000000080000000a00000040010000f00000000400020000000000080000000b0000000000000001000000050002000000000028000000130000000100000000000000080000000b0000003c0000000100000001000000010000003c00000001000000" | |||
| 14 | + | goldenEnumFormatMulti = "d00000000f000000030004000300000001000000000000000400000003000000020000000000000002000000000000000400000003000000010000000000000001000200000000001c000000130000000300000000000000040000000300000008000000080000000c000000000000000300020000000000080000000a00000080070000380400000400020000000000080000000b0000000000000001000000050002000000000028000000130000000100000000000000080000000b000000900000000100000001000000010000009000000001000000" | |||
| 15 | + | goldenBuffers = "c00000000f000000040004000500000001000000000000001c0000001300000001000000000000000400000004000000080000000200000010000000000000000200000000000000040000000400000001000000000000000300000000000000040000000400000000b004000000000004000000000000000400000004000000000500000000000005000000000000000400000004000000100000000000000006000000000000001400000013000000040000000000000004000000040000000400000000000000" | |||
| 16 | + | goldenMetaHeader = "380000000f0000000500040006000000010000000000000004000000030000000100000000000000020000000000000004000000040000002000000000000000" | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | func mustHex(t testing.TB, s string) []byte { | |||
| 20 | + | t.Helper() | |||
| 21 | + | b, err := hex.DecodeString(s) | |||
| 22 | + | if err != nil { | |||
| 23 | + | t.Fatal(err) | |||
| 24 | + | } | |||
| 25 | + | return b | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | func TestVideoEnumFormatGoldenSingle(t *testing.T) { | |||
| 29 | + | var dst [512]byte | |||
| 30 | + | got, err := VideoEnumFormat(dst[:], 320, 240, 60, VideoFormatBGRx) | |||
| 31 | + | if err != nil { | |||
| 32 | + | t.Fatal(err) | |||
| 33 | + | } | |||
| 34 | + | if want := mustHex(t, goldenEnumFormatSingle); hex.EncodeToString(got) != hex.EncodeToString(want) { | |||
| 35 | + | t.Fatalf("got %x\nwant %x", got, want) | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | func TestVideoEnumFormatGoldenMulti(t *testing.T) { | |||
| 40 | + | var dst [512]byte | |||
| 41 | + | got, err := VideoEnumFormat(dst[:], 1920, 1080, 144, VideoFormatBGRx, VideoFormatBGRA) | |||
| 42 | + | if err != nil { | |||
| 43 | + | t.Fatal(err) | |||
| 44 | + | } | |||
| 45 | + | if want := mustHex(t, goldenEnumFormatMulti); hex.EncodeToString(got) != hex.EncodeToString(want) { | |||
| 46 | + | t.Fatalf("got %x\nwant %x", got, want) | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func TestVideoBuffersGolden(t *testing.T) { | |||
| 51 | + | var dst [512]byte | |||
| 52 | + | got, err := VideoBuffers(dst[:], 320*240*4, 320*4, 2, 8, 16, 1<<DataMemFd) | |||
| 53 | + | if err != nil { | |||
| 54 | + | t.Fatal(err) | |||
| 55 | + | } | |||
| 56 | + | if want := mustHex(t, goldenBuffers); hex.EncodeToString(got) != hex.EncodeToString(want) { | |||
| 57 | + | t.Fatalf("got %x\nwant %x", got, want) | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | func TestMetaHeaderGolden(t *testing.T) { | |||
| 62 | + | var dst [128]byte | |||
| 63 | + | got, err := MetaHeader(dst[:]) | |||
| 64 | + | if err != nil { | |||
| 65 | + | t.Fatal(err) | |||
| 66 | + | } | |||
| 67 | + | if want := mustHex(t, goldenMetaHeader); hex.EncodeToString(got) != hex.EncodeToString(want) { | |||
| 68 | + | t.Fatalf("got %x\nwant %x", got, want) | |||
| 69 | + | } | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // formatPod builds a negotiated (non-choice) Format object for parser tests. | |||
| 73 | + | func formatPod(t testing.TB, dst []byte, format VideoFormat, w, h uint32) []byte { | |||
| 74 | + | t.Helper() | |||
| 75 | + | b := NewPodBuilder(dst) | |||
| 76 | + | b.Object(TypeObjectFormat, ParamFormat) | |||
| 77 | + | b.Prop(FormatKeyMediaType) | |||
| 78 | + | b.Id(MediaTypeVideo) | |||
| 79 | + | b.Prop(FormatKeyMediaSubtype) | |||
| 80 | + | b.Id(MediaSubtypeRaw) | |||
| 81 | + | b.Prop(FormatKeyVideoFormat) | |||
| 82 | + | b.Id(uint32(format)) | |||
| 83 | + | b.Prop(FormatKeyVideoSize) | |||
| 84 | + | b.Rectangle(w, h) | |||
| 85 | + | b.Prop(FormatKeyVideoFramerate) | |||
| 86 | + | b.Fraction(60, 1) | |||
| 87 | + | b.End() | |||
| 88 | + | pod, err := b.Bytes() | |||
| 89 | + | if err != nil { | |||
| 90 | + | t.Fatal(err) | |||
| 91 | + | } | |||
| 92 | + | return pod | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | func TestParseVideoFormatRoundTrip(t *testing.T) { | |||
| 96 | + | var dst [256]byte | |||
| 97 | + | for _, tc := range []struct { | |||
| 98 | + | f VideoFormat | |||
| 99 | + | w, h int | |||
| 100 | + | }{{VideoFormatBGRx, 320, 240}, {VideoFormatBGRA, 3840, 2160}} { | |||
| 101 | + | pod := formatPod(t, dst[:], tc.f, uint32(tc.w), uint32(tc.h)) | |||
| 102 | + | f, w, h, ok := ParseVideoFormat(pod) | |||
| 103 | + | if !ok || VideoFormat(f) != tc.f || w != tc.w || h != tc.h { | |||
| 104 | + | t.Fatalf("parse = %d %d %d %v, want %d %d %d", f, w, h, ok, tc.f, tc.w, tc.h) | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | func TestParseVideoFormatRejectsMalformed(t *testing.T) { | |||
| 110 | + | var dst [256]byte | |||
| 111 | + | pod := append([]byte(nil), formatPod(t, dst[:], VideoFormatBGRx, 320, 240)...) | |||
| 112 | + | ||||
| 113 | + | // Every truncation must be rejected without panicking. | |||
| 114 | + | for n := 0; n < len(pod); n++ { | |||
| 115 | + | if _, _, _, ok := ParseVideoFormat(pod[:n]); ok { | |||
| 116 | + | t.Fatalf("truncated to %d bytes accepted", n) | |||
| 117 | + | } | |||
| 118 | + | } | |||
| 119 | + | // Non-object, wrong object type, oversized declared size. | |||
| 120 | + | bad := append([]byte(nil), pod...) | |||
| 121 | + | binary.LittleEndian.PutUint32(bad[4:], TypeInt) | |||
| 122 | + | if _, _, _, ok := ParseVideoFormat(bad); ok { | |||
| 123 | + | t.Fatal("non-object accepted") | |||
| 124 | + | } | |||
| 125 | + | bad = append([]byte(nil), pod...) | |||
| 126 | + | binary.LittleEndian.PutUint32(bad[8:], TypeObjectParamBuffers) | |||
| 127 | + | if _, _, _, ok := ParseVideoFormat(bad); ok { | |||
| 128 | + | t.Fatal("wrong object type accepted") | |||
| 129 | + | } | |||
| 130 | + | bad = append([]byte(nil), pod...) | |||
| 131 | + | binary.LittleEndian.PutUint32(bad[0:], 1<<30) | |||
| 132 | + | if _, _, _, ok := ParseVideoFormat(bad); ok { | |||
| 133 | + | t.Fatal("oversized pod accepted") | |||
| 134 | + | } | |||
| 135 | + | // Property value size overflowing the object. | |||
| 136 | + | bad = append([]byte(nil), pod...) | |||
| 137 | + | binary.LittleEndian.PutUint32(bad[16+8:], 1<<31) | |||
| 138 | + | if _, _, _, ok := ParseVideoFormat(bad); ok { | |||
| 139 | + | t.Fatal("oversized property accepted") | |||
| 140 | + | } | |||
| 141 | + | // Zero and absurd sizes. | |||
| 142 | + | if _, _, _, ok := ParseVideoFormat(formatPod(t, dst[:], VideoFormatBGRx, 0, 240)); ok { | |||
| 143 | + | t.Fatal("zero width accepted") | |||
| 144 | + | } | |||
| 145 | + | if _, _, _, ok := ParseVideoFormat(formatPod(t, dst[:], VideoFormatBGRx, 1<<24, 240)); ok { | |||
| 146 | + | t.Fatal("huge width accepted") | |||
| 147 | + | } | |||
| 148 | + | // An EnumFormat with a choice for the format is not a negotiated format. | |||
| 149 | + | var big [512]byte | |||
| 150 | + | enum, err := VideoEnumFormat(big[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA) | |||
| 151 | + | if err != nil { | |||
| 152 | + | t.Fatal(err) | |||
| 153 | + | } | |||
| 154 | + | if _, _, _, ok := ParseVideoFormat(enum); ok { | |||
| 155 | + | t.Fatal("choice format accepted") | |||
| 156 | + | } | |||
| 157 | + | } | |||
| 158 | + | ||||
| 159 | + | func TestPodBuilderErrors(t *testing.T) { | |||
| 160 | + | // Buffer too small: error, no panic, for every size up to the full pod. | |||
| 161 | + | var full [512]byte | |||
| 162 | + | pod, err := VideoEnumFormat(full[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA) | |||
| 163 | + | if err != nil { | |||
| 164 | + | t.Fatal(err) | |||
| 165 | + | } | |||
| 166 | + | for n := 0; n < len(pod); n++ { | |||
| 167 | + | small := make([]byte, n) | |||
| 168 | + | if _, err := VideoEnumFormat(small, 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA); err != ErrPodFull { | |||
| 169 | + | t.Fatalf("size %d: err = %v, want ErrPodFull", n, err) | |||
| 170 | + | } | |||
| 171 | + | } | |||
| 172 | + | if _, err := VideoBuffers(make([]byte, 16), 1, 1, 2, 8, 16, 4); err != ErrPodFull { | |||
| 173 | + | t.Fatalf("buffers small: %v", err) | |||
| 174 | + | } | |||
| 175 | + | if _, err := MetaHeader(make([]byte, 8)); err != ErrPodFull { | |||
| 176 | + | t.Fatalf("meta small: %v", err) | |||
| 177 | + | } | |||
| 178 | + | // Invalid arguments. | |||
| 179 | + | if _, err := VideoEnumFormat(full[:], 0, 240, 60, VideoFormatBGRx); err == nil { | |||
| 180 | + | t.Fatal("zero width accepted") | |||
| 181 | + | } | |||
| 182 | + | if _, err := VideoEnumFormat(full[:], 320, 240, 60); err == nil { | |||
| 183 | + | t.Fatal("no formats accepted") | |||
| 184 | + | } | |||
| 185 | + | if _, err := VideoBuffers(full[:], 0, 4, 2, 8, 16, 4); err == nil { | |||
| 186 | + | t.Fatal("zero size accepted") | |||
| 187 | + | } | |||
| 188 | + | // Unbalanced builder use. | |||
| 189 | + | b := NewPodBuilder(full[:]) | |||
| 190 | + | b.Prop(1) | |||
| 191 | + | if _, err := b.Bytes(); err != ErrPodState { | |||
| 192 | + | t.Fatalf("prop outside object: %v", err) | |||
| 193 | + | } | |||
| 194 | + | b = NewPodBuilder(full[:]) | |||
| 195 | + | b.Object(TypeObjectFormat, ParamFormat) | |||
| 196 | + | if _, err := b.Bytes(); err != ErrPodState { | |||
| 197 | + | t.Fatalf("open object: %v", err) | |||
| 198 | + | } | |||
| 199 | + | b = NewPodBuilder(full[:]) | |||
| 200 | + | b.End() | |||
| 201 | + | if _, err := b.Bytes(); err != ErrPodState { | |||
| 202 | + | t.Fatalf("end without object: %v", err) | |||
| 203 | + | } | |||
| 204 | + | } | |||
| 205 | + | ||||
| 206 | + | func TestPodAllocations(t *testing.T) { | |||
| 207 | + | var dst [512]byte | |||
| 208 | + | pod := append([]byte(nil), formatPod(t, dst[:], VideoFormatBGRx, 320, 240)...) | |||
| 209 | + | allocs := testing.AllocsPerRun(200, func() { | |||
| 210 | + | e, _ := VideoEnumFormat(dst[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA) | |||
| 211 | + | _, _ = VideoBuffers(dst[:], 307200, 1280, 2, 8, 16, 1<<DataMemFd) | |||
| 212 | + | _, _ = MetaHeader(dst[:]) | |||
| 213 | + | _ = e | |||
| 214 | + | _, _, _, _ = ParseVideoFormat(pod) | |||
| 215 | + | }) | |||
| 216 | + | if allocs != 0 { | |||
| 217 | + | t.Fatalf("allocs = %v, want 0", allocs) | |||
| 218 | + | } | |||
| 219 | + | } | |||