aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/pipewire/core.go
blob: 80bd217f388c2aa5efabbe0d55256a856930c07a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
package pipewire

import (
	"errors"
	"fmt"
	"maps"
	"slices"
	"strconv"
	"syscall"
	"time"
)

/* pipewire/core.h */

const (
	PW_TYPE_INTERFACE_Core     = PW_TYPE_INFO_INTERFACE_BASE + "Core"
	PW_TYPE_INTERFACE_Registry = PW_TYPE_INFO_INTERFACE_BASE + "Registry"
	PW_CORE_PERM_MASK          = PW_PERM_R | PW_PERM_X | PW_PERM_M
	PW_VERSION_CORE            = 4
	PW_VERSION_REGISTRY        = 3

	PW_DEFAULT_REMOTE = "pipewire-0"
	PW_ID_CORE        = 0
	PW_ID_ANY         = Word(0xffffffff)
)

const (
	PW_CORE_CHANGE_MASK_PROPS = 1 << iota

	PW_CORE_CHANGE_MASK_ALL = 1<<iota - 1
)

const (
	PW_CORE_EVENT_INFO = iota
	PW_CORE_EVENT_DONE
	PW_CORE_EVENT_PING
	PW_CORE_EVENT_ERROR
	PW_CORE_EVENT_REMOVE_ID
	PW_CORE_EVENT_BOUND_ID
	PW_CORE_EVENT_ADD_MEM
	PW_CORE_EVENT_REMOVE_MEM
	PW_CORE_EVENT_BOUND_PROPS

	PW_CORE_EVENT_NUM
	PW_VERSION_CORE_EVENTS = 1
)

const (
	PW_CORE_METHOD_ADD_LISTENER = iota

	PW_CORE_METHOD_HELLO
	PW_CORE_METHOD_SYNC
	PW_CORE_METHOD_PONG
	PW_CORE_METHOD_ERROR
	PW_CORE_METHOD_GET_REGISTRY
	PW_CORE_METHOD_CREATE_OBJECT
	PW_CORE_METHOD_DESTROY

	PW_CORE_METHOD_NUM
	PW_VERSION_CORE_METHODS = 0
)

const (
	PW_REGISTRY_EVENT_GLOBAL = iota
	PW_REGISTRY_EVENT_GLOBAL_REMOVE

	PW_REGISTRY_EVENT_NUM
	PW_VERSION_REGISTRY_EVENTS = 0
)

const (
	PW_REGISTRY_METHOD_ADD_LISTENER = iota

	PW_REGISTRY_METHOD_BIND
	PW_REGISTRY_METHOD_DESTROY

	PW_REGISTRY_METHOD_NUM
	PW_VERSION_REGISTRY_METHODS = 0
)

const (
	FOOTER_CORE_OPCODE_GENERATION = iota

	FOOTER_CORE_OPCODE_LAST
)

// The FooterCoreGeneration indicates to the client what is the current
// registry generation number of the Context on the server side.
//
// The server shall include this footer in the next message it sends that
// follows the increment of the registry generation number.
type FooterCoreGeneration struct {
	RegistryGeneration Long `json:"registry_generation"`
}

// Size satisfies [KnownSize] with a constant value.
func (fcg FooterCoreGeneration) Size() Word {
	return SizePrefix +
		Size(SizeLong)
}

// The FooterClientGeneration indicates to the server what is the last
// registry generation number the client has processed.
//
// The client shall include this footer in the next message it sends,
// after it has processed an incoming message whose footer includes a
// registry generation update.
type FooterClientGeneration struct {
	ClientGeneration Long `json:"client_generation"`
}

// Size satisfies [KnownSize] with a constant value.
func (fcg FooterClientGeneration) Size() Word {
	return SizePrefix +
		Size(SizeLong)
}

// A CoreInfo event is emitted by the server upon connection
// with the more information about the server.
type CoreInfo struct {
	// The id of the server (PW_ID_CORE).
	ID Int `json:"id"`
	// A unique cookie for this server.
	Cookie Int `json:"cookie"`
	// The name of the user running the server.
	UserName String `json:"user_name"`
	// The name of the host running the server.
	HostName String `json:"host_name"`
	// A version string of the server.
	Version String `json:"version"`
	// The name of the server.
	Name String `json:"name"`
	// A set of bits with changes to the info.
	ChangeMask Long `json:"change_mask"`
	// Optional key/value properties, valid when change_mask has PW_CORE_CHANGE_MASK_PROPS.
	Properties *SPADict `json:"props"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreInfo) Opcode() byte { return PW_CORE_EVENT_INFO }

// FileCount satisfies [Message] with a constant value.
func (c *CoreInfo) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a value computed at runtime.
func (c *CoreInfo) Size() Word {
	return SizePrefix +
		Size(SizeInt) +
		Size(SizeInt) +
		SizeString[Word](c.UserName) +
		SizeString[Word](c.HostName) +
		SizeString[Word](c.Version) +
		SizeString[Word](c.Name) +
		Size(SizeLong) +
		c.Properties.Size()
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreInfo) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreInfo) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// The CoreDone event is emitted as a result of a client Sync method.
type CoreDone struct {
	// Passed from [CoreSync.ID].
	ID Int `json:"id"`
	// Passed from [CoreSync.Sequence].
	Sequence Int `json:"seq"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreDone) Opcode() byte { return PW_CORE_EVENT_DONE }

// FileCount satisfies [Message] with a constant value.
func (c *CoreDone) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CoreDone) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreDone) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreDone) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// The CorePing event is emitted by the server when it wants to check if a client is
// alive or ensure that it has processed the previous events.
type CorePing struct {
	// The object id to ping.
	ID Int `json:"id"`
	// Usually automatically generated.
	// The client should pass this in the Pong method reply.
	Sequence Int `json:"seq"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CorePing) Opcode() byte { return PW_CORE_EVENT_PING }

// FileCount satisfies [Message] with a constant value.
func (c *CorePing) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CorePing) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CorePing) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CorePing) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// The CoreError can be emitted by both the client and the server.
//
// When emitted by the server, the error event is sent out when a fatal
// (non-recoverable) error has occurred. The id argument is the proxy
// object where the error occurred, most often in response to a request
// to that object. The message is a brief description of the error, for
// (debugging) convenience.
//
// When emitted by the client, it indicates an error occurred in an
// object on the client.
type CoreError struct {
	// The id of the resource (proxy if emitted by the client) that is in error.
	ID Int `json:"id"`
	// A seq number from the failing request (if any).
	Sequence Int `json:"seq"`
	// A negative errno style error code.
	Result Int `json:"res"`
	// An error message.
	Message String `json:"message"`
}

// FileCount satisfies [Message] with a constant value.
func (c *CoreError) FileCount() Int { return 0 }

func (c *CoreError) Error() string {
	return "received Core::Error on" +
		" id " + strconv.Itoa(int(c.ID)) +
		" seq " + strconv.Itoa(int(c.Sequence)) +
		" res " + strconv.Itoa(int(c.Result)) +
		": " + c.Message
}

// Size satisfies [KnownSize] with a value computed at runtime.
func (c *CoreError) Size() Word {
	return SizePrefix +
		Size(SizeInt) +
		Size(SizeInt) +
		Size(SizeInt) +
		SizeString[Word](c.Message)
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreError) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreError) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// CoreErrorMethod is [CoreError] as a method [Message].
type CoreErrorMethod struct{ CoreError }

// Opcode satisfies [Message] with a constant value.
func (c *CoreErrorMethod) Opcode() byte { return PW_CORE_METHOD_ERROR }

// CoreErrorEvent is [CoreError] as an event [Message].
type CoreErrorEvent struct{ CoreError }

// Opcode satisfies [Message] with a constant value.
func (c *CoreErrorEvent) Opcode() byte { return PW_CORE_EVENT_ERROR }

// The CoreRemoveId event is used internally by the object ID management logic.
//
// When a client deletes an object, the server will send this event to acknowledge
// that it has seen the delete request. When the client receives this event, it
// will know that it can safely reuse the object ID.
type CoreRemoveId struct {
	// A proxy id that was removed.
	ID Int `json:"id"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreRemoveId) Opcode() byte { return PW_CORE_EVENT_REMOVE_ID }

// FileCount satisfies [Message] with a constant value.
func (c *CoreRemoveId) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CoreRemoveId) Size() Word { return SizePrefix + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreRemoveId) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreRemoveId) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// The CoreBoundProps event is emitted when a local object ID is bound to a global ID.
// It is emitted before the global becomes visible in the registry.
type CoreBoundProps struct {
	// A proxy id.
	ID Int `json:"id"`
	// The global_id as it will appear in the registry.
	GlobalID Int `json:"global_id"`
	// The properties of the global.
	Properties *SPADict `json:"props"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreBoundProps) Opcode() byte { return PW_CORE_EVENT_BOUND_PROPS }

// FileCount satisfies [Message] with a constant value.
func (c *CoreBoundProps) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a value computed at runtime.
func (c *CoreBoundProps) Size() Word {
	return SizePrefix +
		Size(SizeInt) +
		Size(SizeInt) +
		c.Properties.Size()
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreBoundProps) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreBoundProps) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// ErrBadBoundProps is returned when a [CoreBoundProps] event targeting a proxy
// that should never be targeted is received and processed.
var ErrBadBoundProps = errors.New("attempting to store bound props on a proxy that should never be targeted")

// noAck is embedded by proxies that are never targeted by [CoreBoundProps].
type noAck struct{}

// setBoundProps should never be called as this proxy should never be targeted by [CoreBoundProps].
func (noAck) setBoundProps(*CoreBoundProps) error { panic(ErrBadBoundProps) }

// ErrBadRemove is returned when a [CoreRemoveId] event targeting a proxy
// that should never be targeted is received and processed.
var ErrBadRemove = errors.New("attempting to remove a proxy that should never be targeted")

// noRemove is embedded by proxies that are never targeted by [CoreRemoveId].
type noRemove struct{}

// remove should never be called as this proxy should never be targeted by [CoreRemoveId].
func (noRemove) remove() error { panic(ErrBadRemove) }

// ErrInvalidRemove is returned when a proxy is somehow removed twice. This is only reached for
// an implementation error as the proxy struct should no longer be reachable after the first call.
var ErrInvalidRemove = errors.New("attempting to remove an already freed proxy")

// removable is embedded by proxies that can be targeted by [CoreRemoveId] and requires no cleanup.
type removable bool

// remove checks against removal of a freed proxy and marks the proxy as removed.
func (s *removable) remove() error {
	if *s {
		panic(ErrInvalidRemove)
	}
	*s = true
	return nil
}

// ErrProxyDestroyed is returned when attempting to use a proxy method when the underlying
// proxy has already been targeted by a [CoreRemoveId] event.
var ErrProxyDestroyed = errors.New("underlying proxy has been removed")

// checkDestroy returns [ErrProxyDestroyed] if the current proxy has been destroyed.
// Must be called at the beginning of any exported method of a proxy embedding removable.
func (s *removable) checkDestroy() error {
	if *s {
		// not fatal: the caller is allowed to recover from this and allocate a new proxy
		return ErrProxyDestroyed
	}
	return nil
}

// mustCheckDestroy calls checkDestroy and panics if a non-nil error is returned.
// This is useful for non-exported methods as they should become unreachable.
func (s *removable) mustCheckDestroy() {
	if err := s.checkDestroy(); err != nil {
		panic(err)
	}
}

// An InconsistentIdError describes an inconsistent state where the server claims an impossible
// proxy or global id. This is only generated by the [CoreBoundProps] event.
type InconsistentIdError struct {
	// Whether the inconsistent id is the global resource id.
	Global bool
	// Targeted proxy instance.
	Proxy fmt.Stringer
	// Differing ids.
	ID, ServerID Int
}

func (e *InconsistentIdError) Error() string {
	name := "proxy"
	if e.Global {
		name = "global"
	}

	return name + " id " + strconv.Itoa(int(e.ID)) + " targeting " + e.Proxy.String() +
		" inconsistent with " + strconv.Itoa(int(e.ServerID)) + " claimed by the server"
}

// CoreHello is the first message sent by a client.
type CoreHello struct {
	// The version number of the client, usually PW_VERSION_CORE.
	Version Int `json:"version"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreHello) Opcode() byte { return PW_CORE_METHOD_HELLO }

// FileCount satisfies [Message] with a constant value.
func (c *CoreHello) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CoreHello) Size() Word { return SizePrefix + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreHello) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreHello) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// hello queues a [CoreHello] message for the PipeWire server.
// This method should not be called directly, the [New] function queues this message.
func (core *Core) hello() error {
	return core.ctx.writeMessage(
		PW_ID_CORE,
		&CoreHello{PW_VERSION_CORE},
	)
}

const (
	// CoreSyncSequenceOffset is the offset to [Header.Sequence] to produce [CoreSync.Sequence].
	CoreSyncSequenceOffset = 0x40000000
)

// The CoreSync message will result in a Done event from the server.
// When the Done event is received, the client can be sure that all
// operations before the Sync method have been completed.
type CoreSync struct {
	// The id will be returned in the Done event.
	ID Int `json:"id"`
	// Usually generated automatically and will be returned in the Done event.
	Sequence Int `json:"seq"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreSync) Opcode() byte { return PW_CORE_METHOD_SYNC }

// FileCount satisfies [Message] with a constant value.
func (c *CoreSync) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CoreSync) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreSync) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreSync) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// sync queues a [CoreSync] message for the PipeWire server.
// This is not safe to use directly, callers should use Sync instead.
func (core *Core) sync(id Int) error {
	return core.ctx.writeMessage(
		PW_ID_CORE,
		&CoreSync{id, CoreSyncSequenceOffset + Int(core.ctx.sequence)},
	)
}

// ErrNotDone is returned if [Core.Sync] returns from its [Context.Roundtrip] without
// receiving a [CoreDone] event targeting the [CoreSync] event it delivered.
var ErrNotDone = errors.New("did not receive a Core::Done event targeting previously delivered Core::Sync")

const (
	// syncTimeout is the maximum duration [Core.Sync] is allowed to take before
	// receiving [CoreDone] or failing.
	syncTimeout = 5 * time.Second
)

// Sync queues a [CoreSync] message for the PipeWire server and initiates a Roundtrip.
func (core *Core) Sync() error {
	core.done = false
	if err := core.sync(roundtripSyncID); err != nil {
		return err
	}
	deadline := time.Now().Add(syncTimeout)

	for !core.done {
		if time.Now().After(deadline) {
			return ErrNotDone
		}

		if err := core.ctx.roundtrip(); err != nil {
			return err
		}
	}

	if len(core.ctx.pendingIds) != 0 {
		core.ctx.closeReceivedFiles()
		return &ProxyFatalError{Err: UnacknowledgedProxyError(slices.Collect(maps.Keys(core.ctx.pendingIds))), ProxyErrs: core.ctx.cloneAsProxyErrors()}
	}
	return core.ctx.doSyncComplete()
}

// The CorePong message is sent from the client to the server when the server emits the Ping event.
type CorePong struct {
	// Copied from [CorePing.ID].
	ID Int `json:"id"`
	// Copied from [CorePing.Sequence]
	Sequence Int `json:"seq"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CorePong) Opcode() byte { return PW_CORE_METHOD_PONG }

// FileCount satisfies [Message] with a constant value.
func (c *CorePong) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CorePong) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CorePong) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CorePong) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// CoreGetRegistry is sent when a client requests to bind to the
// registry object and list the available objects on the server.
//
// Like with all bindings, first the client allocates a new proxy
// id and puts this as the new_id field. Methods and Events can
// then be sent and received on the new_id (in the message Id field).
type CoreGetRegistry struct {
	// The version of the registry interface used on the client,
	// usually PW_VERSION_REGISTRY.
	Version Int `json:"version"`
	// The id of the new proxy with the registry interface,
	// ends up as [Header.ID] in future messages.
	NewID Int `json:"new_id"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreGetRegistry) Opcode() byte { return PW_CORE_METHOD_GET_REGISTRY }

// FileCount satisfies [Message] with a constant value.
func (c *CoreGetRegistry) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CoreGetRegistry) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreGetRegistry) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreGetRegistry) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// GetRegistry queues a [CoreGetRegistry] message for the PipeWire server
// and returns the address of the newly allocated [Registry].
func (ctx *Context) GetRegistry() (*Registry, error) {
	registry := Registry{Objects: make(map[Int]RegistryGlobal), ctx: ctx}
	newId := ctx.newProxyId(&registry, false)
	registry.ID = newId
	return &registry, ctx.writeMessage(
		PW_ID_CORE,
		&CoreGetRegistry{PW_VERSION_REGISTRY, newId},
	)
}

// A RegistryGlobal event is emitted to notify a client about a new global object.
type RegistryGlobal struct {
	// The global id.
	ID Int `json:"id"`
	// Permission bits.
	Permissions Int `json:"permissions"`
	// The type of object.
	Type String `json:"type"`
	// The server version of the object.
	Version Int `json:"version"`
	// Extra global properties.
	Properties *SPADict `json:"props"`
}

// Opcode satisfies [Message] with a constant value.
func (c *RegistryGlobal) Opcode() byte { return PW_REGISTRY_EVENT_GLOBAL }

// FileCount satisfies [Message] with a constant value.
func (c *RegistryGlobal) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a value computed at runtime.
func (c *RegistryGlobal) Size() Word {
	return SizePrefix +
		Size(SizeInt) +
		Size(SizeInt) +
		SizeString[Word](c.Type) +
		Size(SizeInt) +
		c.Properties.Size()
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *RegistryGlobal) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryGlobal) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// A RegistryGlobalRemove event is emitted when a global with id was removed.
type RegistryGlobalRemove struct {
	// The global id that was removed.
	ID Int `json:"id"`
}

// Opcode satisfies [Message] with a constant value.
func (c *RegistryGlobalRemove) Opcode() byte { return PW_REGISTRY_EVENT_GLOBAL_REMOVE }

// FileCount satisfies [Message] with a constant value.
func (c *RegistryGlobalRemove) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *RegistryGlobalRemove) Size() Word {
	return SizePrefix +
		Size(SizeInt)
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *RegistryGlobalRemove) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryGlobalRemove) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// RegistryBind is sent when the client requests to bind to the
// global object with id and use the client proxy with new_id as
// the proxy. After this call, methods can be sent to the remote
// global object and events can be received.
type RegistryBind struct {
	// The [RegistryGlobal.ID] to bind to.
	ID Int `json:"id"`
	// the [RegistryGlobal.Type] of the global id.
	Type String `json:"type"`
	// The client version of the interface for type.
	Version Int `json:"version"`
	// The client proxy id for the global object.
	NewID Int `json:"new_id"`
}

// Opcode satisfies [Message] with a constant value.
func (c *RegistryBind) Opcode() byte { return PW_REGISTRY_METHOD_BIND }

// FileCount satisfies [Message] with a constant value.
func (c *RegistryBind) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a value computed at runtime.
func (c *RegistryBind) Size() Word {
	return SizePrefix +
		Size(SizeInt) +
		SizeString[Word](c.Type) +
		Size(SizeInt) +
		Size(SizeInt)
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *RegistryBind) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryBind) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// bind queues a [RegistryBind] message for the PipeWire server
// and returns the newly allocated proxy id.
func (registry *Registry) bind(proxy eventProxy, id, version Int) (Int, error) {
	bind := RegistryBind{
		ID:      id,
		Type:    proxy.String(),
		Version: version,
		NewID:   registry.ctx.newProxyId(proxy, true),
	}
	return bind.NewID, registry.ctx.writeMessage(
		registry.ID,
		&bind,
	)
}

// RegistryDestroy is sent to try to destroy the global object with id.
// This might fail when the client does not have permission.
type RegistryDestroy struct {
	// The global id to destroy.
	ID Int `json:"id"`
}

// Opcode satisfies [Message] with a constant value.
func (c *RegistryDestroy) Opcode() byte { return PW_REGISTRY_METHOD_DESTROY }

// FileCount satisfies [Message] with a constant value.
func (c *RegistryDestroy) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *RegistryDestroy) Size() Word {
	return SizePrefix +
		Size(SizeInt)
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *RegistryDestroy) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryDestroy) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// destroy queues a [RegistryDestroy] message for the PipeWire server.
func (registry *Registry) destroy(id Int) error {
	return registry.ctx.writeMessage(
		registry.ID,
		&RegistryDestroy{id},
	)
}

// Destroy tries to destroy the global object with id.
func (registry *Registry) Destroy(id Int) (err error) {
	asCoreError := registry.ctx.expectsCoreError(registry.ID, &err)
	if err != nil {
		return
	}
	if err = registry.destroy(id); err != nil {
		return err
	}
	if err = registry.ctx.GetCore().Sync(); err == nil {
		return nil
	}

	if coreError := asCoreError(); coreError == nil {
		return
	} else {
		switch syscall.Errno(-coreError.Result) {
		case syscall.EPERM:
			return &PermissionError{registry.ID, coreError.Message}

		default:
			return coreError
		}
	}
}

// An UnsupportedObjectTypeError is the name of a type not known by the server [Registry].
type UnsupportedObjectTypeError string

func (e UnsupportedObjectTypeError) Error() string   { return "unsupported object type " + string(e) }
func (e UnsupportedObjectTypeError) Message() string { return e.Error() }

// Core holds state of [PW_TYPE_INTERFACE_Core].
type Core struct {
	// Additional information from the server, populated or updated during [Context.Roundtrip].
	Info *CoreInfo `json:"info"`

	// Whether a [CoreDone] event was received during Sync.
	done bool

	ctx *Context

	noAck
	noRemove
}

// ErrUnexpectedDone is a [CoreDone] event with unexpected values.
var ErrUnexpectedDone = errors.New("multiple Core::Done events targeting Core::Sync")

// An UnknownProxyIdError describes an event targeting a proxy id that was never allocated.
type UnknownProxyIdError[E any] struct {
	// Offending id decoded from Data.
	Id Int
	// Event received from the server.
	Event E
}

func (e *UnknownProxyIdError[E]) Error() string {
	return "unknown proxy id " + strconv.Itoa(int(e.Id))
}

// An InvalidPingError is a [CorePing] event targeting a proxy id that was never allocated.
type InvalidPingError CorePing

func (e *InvalidPingError) Error() string {
	return "received Core::Ping seq " + strconv.Itoa(int(e.Sequence)) +
		" targeting unknown proxy id " + strconv.Itoa(int(e.ID))
}

func (core *Core) consume(opcode byte, files []int, unmarshal func(v any)) error {
	closeReceivedFiles(files...)
	switch opcode {
	case PW_CORE_EVENT_INFO:
		unmarshal(&core.Info)
		return nil

	case PW_CORE_EVENT_DONE:
		var done CoreDone
		unmarshal(&done)
		if done.ID == roundtripSyncID && done.Sequence == CoreSyncSequenceOffset+core.ctx.currentSeq() {
			if core.done {
				return ErrUnexpectedDone
			}
			core.done = true
		}

		// silently ignore non-matching events because the server sends out
		// an event with id -1 seq 0 that does not appear to correspond to
		// anything, and this behaviour is never mentioned in documentation
		return nil

	case PW_CORE_EVENT_PING:
		var ping CorePing
		unmarshal(&ping)
		if _, ok := core.ctx.proxy[ping.ID]; ok {
			core.ctx.mustWriteMessage(PW_ID_CORE, (*CorePong)(&ping))
			return nil
		} else {
			invalidPingError := InvalidPingError(ping)
			core.ctx.mustWriteMessage(PW_ID_CORE, &CoreErrorMethod{CoreError{
				ID:       PW_ID_CORE,
				Sequence: core.ctx.currentRemoteSeq(),
				Result:   -Int(syscall.EINVAL),
				Message:  invalidPingError.Error(),
			}})
			return &invalidPingError
		}

	case PW_CORE_EVENT_ERROR:
		var coreError CoreError
		unmarshal(&coreError)
		return &coreError

	case PW_CORE_EVENT_REMOVE_ID:
		var coreRemoveId CoreRemoveId
		unmarshal(&coreRemoveId)
		if proxy, ok := core.ctx.proxy[coreRemoveId.ID]; !ok {
			// this should never happen so is non-recoverable if it does
			panic(&UnknownProxyIdError[*CoreRemoveId]{Id: coreRemoveId.ID, Event: &coreRemoveId})
		} else {
			delete(core.ctx.proxy, coreRemoveId.ID)
			return proxy.remove()
		}

	case PW_CORE_EVENT_BOUND_PROPS:
		var boundProps CoreBoundProps
		unmarshal(&boundProps)

		delete(core.ctx.pendingIds, boundProps.ID)
		proxy, ok := core.ctx.proxy[boundProps.ID]
		if !ok {
			return &UnknownProxyIdError[*CoreBoundProps]{Id: boundProps.ID, Event: &boundProps}
		}
		return proxy.setBoundProps(&boundProps)

	default:
		panic(&UnsupportedOpcodeError{opcode, core.String()})
	}
}

func (core *Core) String() string { return PW_TYPE_INTERFACE_Core }

// Registry holds state of [PW_TYPE_INTERFACE_Registry].
type Registry struct {
	// Proxy id as tracked by [Context].
	ID Int `json:"proxy_id"`

	// Global objects received via the [RegistryGlobal] event.
	//
	// This requires more processing before it can be used, but is not implemented
	// as it is not used by Hakurei.
	Objects map[Int]RegistryGlobal `json:"objects"`

	ctx *Context

	noAck
	noRemove
}

// A GlobalIDCollisionError describes a [RegistryGlobal] event stepping on a previous instance of itself.
type GlobalIDCollisionError struct {
	// The colliding id.
	ID Int
	// Involved events.
	Previous, Current *RegistryGlobal
}

func (e *GlobalIDCollisionError) Error() string {
	return "new Registry::Global event for " + e.Current.Type +
		" stepping on previous id " + strconv.Itoa(int(e.ID)) + " for " + e.Previous.Type
}

// An UnknownGlobalIDRemoveError describes a [RegistryGlobalRemove] event announcing the removal of
// a global id that is not yet known to [Registry] or was already deleted.
type UnknownGlobalIDRemoveError Int

func (e UnknownGlobalIDRemoveError) Error() string {
	return "Registry::GlobalRemove event targets unknown id " + strconv.Itoa(int(e))
}

func (registry *Registry) consume(opcode byte, files []int, unmarshal func(v any)) error {
	closeReceivedFiles(files...)
	switch opcode {
	case PW_REGISTRY_EVENT_GLOBAL:
		var global RegistryGlobal
		unmarshal(&global)
		if object, ok := registry.Objects[global.ID]; ok {
			// this should never happen so is non-recoverable if it does
			panic(&GlobalIDCollisionError{global.ID, &object, &global})
		}
		registry.Objects[global.ID] = global
		return nil

	case PW_REGISTRY_EVENT_GLOBAL_REMOVE:
		var globalRemove RegistryGlobalRemove
		unmarshal(&globalRemove)
		// server emits PW_CORE_EVENT_REMOVE_ID events targeting
		// affected proxies so they do not need to be handled here
		l := len(registry.Objects)
		delete(registry.Objects, globalRemove.ID)
		if len(registry.Objects) != l-1 {
			// this should never happen so is non-recoverable if it does
			panic(UnknownGlobalIDRemoveError(globalRemove.ID))
		}
		return nil

	default:
		panic(&UnsupportedOpcodeError{opcode, registry.String()})
	}
}

func (registry *Registry) String() string { return PW_TYPE_INTERFACE_Registry }