1package spa
2
3import (
4 "encoding/binary"
5 "errors"
6 "math"
7)
8
9// Errors returned by the pod builder.
10var (
11 // ErrPodFull is returned when the destination buffer is too small.
12 ErrPodFull = errors.New("spa: pod buffer full")
13 // ErrPodState is returned for unbalanced Object/End or Prop outside an object.
14 ErrPodState = errors.New("spa: invalid pod builder state")
15)
16
17const podMaxDepth = 4
18
19// Fraction is a SPA fraction (num/denom).
20type Fraction struct{ Num, Denom uint32 }
21
22// PodBuilder encodes SPA pods little-endian into a caller-owned buffer. It
23// never allocates and never grows past len(buf): when the buffer is full the
24// first error is kept and returned by Bytes. The zero value is unusable; use
25// NewPodBuilder.
26type PodBuilder struct {
27 buf []byte
28 n int
29 err error
30 depth int
31 stack [podMaxDepth]int // offset of each open object header
32}
33
34// NewPodBuilder returns a builder writing into buf.
35func NewPodBuilder(buf []byte) PodBuilder { return PodBuilder{buf: buf} }
36
37// Bytes returns the encoded pod bytes (a prefix of the destination buffer) or
38// the first error. It fails if an Object is still open.
39func (b *PodBuilder) Bytes() ([]byte, error) {
40 if b.err != nil {
41 return nil, b.err
42 }
43 if b.depth != 0 {
44 return nil, ErrPodState
45 }
46 return b.buf[:b.n], nil
47}
48
49func (b *PodBuilder) fail(err error) {
50 if b.err == nil {
51 b.err = err
52 }
53}
54
55// room reports whether n more bytes fit, recording ErrPodFull otherwise.
56func (b *PodBuilder) room(n int) bool {
57 if b.err != nil {
58 return false
59 }
60 if n < 0 || b.n+n > len(b.buf) {
61 b.fail(ErrPodFull)
62 return false
63 }
64 return true
65}
66
67func (b *PodBuilder) u32(v uint32) {
68 binary.LittleEndian.PutUint32(b.buf[b.n:], v)
69 b.n += 4
70}
71
72// pad8 zero-pads the write offset to a multiple of 8 bytes.
73func (b *PodBuilder) pad8() {
74 for b.n&7 != 0 {
75 b.buf[b.n] = 0
76 b.n++
77 }
78}
79
80// Object opens an object pod. Close it with End.
81func (b *PodBuilder) Object(objType, id uint32) {
82 if !b.room(16) {
83 return
84 }
85 if b.depth == podMaxDepth {
86 b.fail(ErrPodState)
87 return
88 }
89 b.stack[b.depth] = b.n
90 b.depth++
91 b.u32(0) // size, patched by End
92 b.u32(TypeObject)
93 b.u32(objType)
94 b.u32(id)
95}
96
97// Prop starts an object property with the given key; the next value written
98// is the property value.
99func (b *PodBuilder) Prop(key uint32) {
100 if b.err == nil && b.depth == 0 {
101 b.fail(ErrPodState)
102 return
103 }
104 if !b.room(8) {
105 return
106 }
107 b.u32(key)
108 b.u32(0) // flags
109}
110
111// End closes the innermost open object.
112func (b *PodBuilder) End() {
113 if b.err != nil {
114 return
115 }
116 if b.depth == 0 {
117 b.fail(ErrPodState)
118 return
119 }
120 b.depth--
121 start := b.stack[b.depth]
122 binary.LittleEndian.PutUint32(b.buf[start:], uint32(b.n-start-8))
123 // Padding was already room-checked by the last value; objects always
124 // end on an 8-byte boundary because every value is padded.
125 b.pad8()
126}
127
128func (b *PodBuilder) scalar(typ uint32, size uint32, a, c uint32) {
129 // size is the body size (4 or 8); the pod is padded to 16 bytes.
130 if !b.room(16) {
131 return
132 }
133 b.u32(size)
134 b.u32(typ)
135 b.u32(a)
136 b.u32(c)
137}
138
139// Id appends an Id pod.
140func (b *PodBuilder) Id(v uint32) { b.scalar(TypeId, 4, v, 0) }
141
142// Int appends an Int pod.
143func (b *PodBuilder) Int(v int32) { b.scalar(TypeInt, 4, uint32(v), 0) }
144
145// Rectangle appends a Rectangle pod.
146func (b *PodBuilder) Rectangle(w, h uint32) { b.scalar(TypeRectangle, 8, w, h) }
147
148// Fraction appends a Fraction pod.
149func (b *PodBuilder) Fraction(num, denom uint32) { b.scalar(TypeFraction, 8, num, denom) }
150
151// choiceBegin writes the choice header; it returns the header offset.
152func (b *PodBuilder) choiceBegin(choiceType, childSize, childType uint32, nvals int) (int, bool) {
153 // Room for the header, the values and the padding choiceEnd adds.
154 if !b.room((b.n+24+nvals*int(childSize)+7)&^7 - b.n) {
155 return 0, false
156 }
157 start := b.n
158 b.u32(0) // size, patched in choiceEnd
159 b.u32(TypeChoice)
160 b.u32(choiceType)
161 b.u32(0) // flags
162 b.u32(childSize)
163 b.u32(childType)
164 return start, true
165}
166
167func (b *PodBuilder) choiceEnd(start int) {
168 binary.LittleEndian.PutUint32(b.buf[start:], uint32(b.n-start-8))
169 b.pad8()
170}
171
172// ChoiceEnumId appends an enum choice of Ids. vals is written verbatim: the
173// first value is the default and the remaining values are the alternatives.
174func (b *PodBuilder) ChoiceEnumId(vals ...uint32) {
175 if len(vals) == 0 {
176 b.fail(ErrPodState)
177 return
178 }
179 start, ok := b.choiceBegin(ChoiceEnum, 4, TypeId, len(vals))
180 if !ok {
181 return
182 }
183 for _, v := range vals {
184 b.u32(v)
185 }
186 b.choiceEnd(start)
187}
188
189// ChoiceRangeInt appends a range choice of Ints.
190func (b *PodBuilder) ChoiceRangeInt(def, min, max int32) {
191 start, ok := b.choiceBegin(ChoiceRange, 4, TypeInt, 3)
192 if !ok {
193 return
194 }
195 b.u32(uint32(def))
196 b.u32(uint32(min))
197 b.u32(uint32(max))
198 b.choiceEnd(start)
199}
200
201// ChoiceRangeFraction appends a range choice of Fractions.
202func (b *PodBuilder) ChoiceRangeFraction(def, min, max Fraction) {
203 start, ok := b.choiceBegin(ChoiceRange, 8, TypeFraction, 3)
204 if !ok {
205 return
206 }
207 for _, f := range [3]Fraction{def, min, max} {
208 b.u32(f.Num)
209 b.u32(f.Denom)
210 }
211 b.choiceEnd(start)
212}
213
214// ChoiceFlagsInt appends a flags choice of Ints with a single flags value.
215func (b *PodBuilder) ChoiceFlagsInt(flags uint32) {
216 start, ok := b.choiceBegin(ChoiceFlags, 4, TypeInt, 1)
217 if !ok {
218 return
219 }
220 b.u32(flags)
221 b.choiceEnd(start)
222}
223
224// VideoEnumFormat encodes an EnumFormat param for raw video of the given
225// size, maximum frame rate and formats (the first format is the default).
226// Modifiers are omitted (shared memory only).
227func VideoEnumFormat(dst []byte, w, h, maxFPS int, formats ...VideoFormat) ([]byte, error) {
228 if len(formats) == 0 || w <= 0 || h <= 0 || maxFPS <= 0 {
229 return nil, ErrPodState
230 }
231 b := NewPodBuilder(dst)
232 b.Object(TypeObjectFormat, ParamEnumFormat)
233 b.Prop(FormatKeyMediaType)
234 b.Id(MediaTypeVideo)
235 b.Prop(FormatKeyMediaSubtype)
236 b.Id(MediaSubtypeRaw)
237 b.Prop(FormatKeyVideoFormat)
238 if len(formats) == 1 {
239 b.Id(uint32(formats[0]))
240 } else {
241 b.podEnumFormats(formats)
242 }
243 b.Prop(FormatKeyVideoSize)
244 b.Rectangle(uint32(w), uint32(h))
245 b.Prop(FormatKeyVideoFramerate)
246 b.Fraction(0, 1)
247 b.Prop(FormatKeyVideoMaxRate)
248 b.ChoiceRangeFraction(
249 Fraction{uint32(maxFPS), 1},
250 Fraction{1, 1},
251 Fraction{uint32(maxFPS), 1},
252 )
253 b.End()
254 return b.Bytes()
255}
256
257// podEnumFormats writes an enum choice of video formats. As in SPA's own
258// builders, the first value is the default and is repeated in the list of
259// alternatives.
260func (b *PodBuilder) podEnumFormats(formats []VideoFormat) {
261 start, ok := b.choiceBegin(ChoiceEnum, 4, TypeId, len(formats)+1)
262 if !ok {
263 return
264 }
265 b.u32(uint32(formats[0]))
266 for _, f := range formats {
267 b.u32(uint32(f))
268 }
269 b.choiceEnd(start)
270}
271
272// VideoBuffers encodes a Buffers param: one block of size bytes with the
273// given stride, a buffer count range (minBuf..maxBuf, defBuf by default) and
274// the allowed SPA data types as a bit mask (for example 1<<DataMemFd).
275func VideoBuffers(dst []byte, size, stride, minBuf, defBuf, maxBuf int, dataTypes uint32) ([]byte, error) {
276 if size <= 0 || stride <= 0 || minBuf <= 0 || defBuf < minBuf || maxBuf < defBuf ||
277 size > math.MaxInt32 || stride > math.MaxInt32 || maxBuf > math.MaxInt32 {
278 return nil, ErrPodState
279 }
280 b := NewPodBuilder(dst)
281 b.Object(TypeObjectParamBuffers, ParamBuffers)
282 b.Prop(BuffersKeyBuffers)
283 b.ChoiceRangeInt(int32(defBuf), int32(minBuf), int32(maxBuf))
284 b.Prop(BuffersKeyBlocks)
285 b.Int(1)
286 b.Prop(BuffersKeySize)
287 b.Int(int32(size))
288 b.Prop(BuffersKeyStride)
289 b.Int(int32(stride))
290 b.Prop(BuffersKeyAlign)
291 b.Int(16)
292 b.Prop(BuffersKeyDataType)
293 b.ChoiceFlagsInt(dataTypes)
294 b.End()
295 return b.Bytes()
296}
297
298// MetaHeader encodes a Meta param requesting a spa_meta_header (32 bytes).
299func MetaHeader(dst []byte) ([]byte, error) {
300 b := NewPodBuilder(dst)
301 b.Object(TypeObjectParamMeta, ParamMeta)
302 b.Prop(MetaKeyType)
303 b.Id(MetaTypeHeader)
304 b.Prop(MetaKeySize)
305 b.Int(MetaHeaderSize)
306 b.End()
307 return b.Bytes()
308}
309
310// ParseVideoFormat extracts the video format and size from a Format param
311// (an Object pod). It does not allocate. ok is false for malformed,
312// truncated or non-raw-video pods.
313//
314// PipeWire wraps every value of a negotiated Format in a Choice of type None
315// holding a single value; plain values are accepted as well. Choices that
316// still offer alternatives (Enum, Range, ...) are not a fixed format and are
317// rejected.
318func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) {
319 if len(pod) < 16 {
320 return 0, 0, 0, false
321 }
322 le := binary.LittleEndian
323 size := int(le.Uint32(pod[0:]))
324 if le.Uint32(pod[4:]) != TypeObject || size < 8 || 8+size > len(pod) {
325 return 0, 0, 0, false
326 }
327 if le.Uint32(pod[8:]) != TypeObjectFormat {
328 return 0, 0, 0, false
329 }
330 end := 8 + size
331 off := 16
332 var haveFmt, haveSize, isVideo, isRaw bool
333 for off+16 <= end {
334 key := le.Uint32(pod[off:])
335 vsize := int(le.Uint32(pod[off+8:]))
336 vtype := le.Uint32(pod[off+12:])
337 body := off + 16
338 if vsize < 0 || body+vsize > end {
339 return 0, 0, 0, false
340 }
341 next := (body + vsize + 7) &^ 7
342 if next <= off {
343 return 0, 0, 0, false
344 }
345 if vtype == TypeChoice {
346 // Choice body: type, flags, then the child pod header (size, type)
347 // and the values.
348 if vsize < 16 || le.Uint32(pod[body:]) != ChoiceNone {
349 return 0, 0, 0, false
350 }
351 childSize := int(le.Uint32(pod[body+8:]))
352 vtype = le.Uint32(pod[body+12:])
353 body += 16
354 vsize -= 16
355 if childSize < 0 || childSize > vsize {
356 return 0, 0, 0, false
357 }
358 vsize = childSize
359 }
360 switch {
361 case key == FormatKeyMediaType && vtype == TypeId && vsize >= 4:
362 isVideo = le.Uint32(pod[body:]) == MediaTypeVideo
363 case key == FormatKeyMediaSubtype && vtype == TypeId && vsize >= 4:
364 isRaw = le.Uint32(pod[body:]) == MediaSubtypeRaw
365 case key == FormatKeyVideoFormat && vtype == TypeId && vsize >= 4:
366 format = le.Uint32(pod[body:])
367 haveFmt = true
368 case key == FormatKeyVideoSize && vtype == TypeRectangle && vsize >= 8:
369 uw, uh := le.Uint32(pod[body:]), le.Uint32(pod[body+4:])
370 if uw == 0 || uh == 0 || uw > 1<<20 || uh > 1<<20 {
371 return 0, 0, 0, false
372 }
373 w, h = int(uw), int(uh)
374 haveSize = true
375 }
376 off = next
377 }
378 if !(haveFmt && haveSize && isVideo && isRaw) {
379 return 0, 0, 0, false
380 }
381 return format, w, h, true
382}