detached c3499e2

feat(spa): add video pod builders and format parser

commit
c3499e28cde9cc10c0fa530a77094fc8d39785d5
author
brice · · 2026-10-14 18:59 UTC
parent
fc13113
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +662 -0
M spa/constants.go +83 -0
@@ -16,3 +16,86 @@ const (
1616 DirectionInput = 0
1717 DirectionOutput = 1
1818 )
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 = 016 DirectionInput = 0
17 DirectionOutput = 117 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close