1package screencast
2
3import (
4 "strconv"
5
6 "github.com/bnema/neferwl/internal/ports"
7)
8
9// session is one org.gnome.Mutter.ScreenCast.Session. The sessions live in a
10// fixed array of Service, so pointers into it (the streams' back references,
11// the PipeWire handlers) stay valid.
12type session struct {
13 used bool
14 id uint32
15 path string
16 owner string // unique name that called CreateSession
17 // started: Start was called, the streams were opened.
18 started bool
19 // dying: the session is closed to the bus; the slot is freed once every
20 // stream released its PipeWire resources.
21 dying bool
22 streams [maxStreams]stream
23}
24
25func (s *Service) sessionByID(id uint32) *session {
26 for i := range s.sessions {
27 if sess := &s.sessions[i]; sess.used && !sess.dying && sess.id == id {
28 return sess
29 }
30 }
31 return nil
32}
33
34func (s *Service) streamByID(id uint32) *stream {
35 for i := range s.sessions {
36 sess := &s.sessions[i]
37 if !sess.used || sess.dying {
38 continue
39 }
40 for j := range sess.streams {
41 if st := &sess.streams[j]; st.used && st.id == id {
42 return st
43 }
44 }
45 }
46 return nil
47}
48
49func (s *Service) forEachSession(f func(*session)) {
50 for i := range s.sessions {
51 if s.sessions[i].used {
52 f(&s.sessions[i])
53 }
54 }
55}
56
57func (s *Service) forEachStream(f func(*stream)) {
58 for i := range s.sessions {
59 sess := &s.sessions[i]
60 if !sess.used {
61 continue
62 }
63 for j := range sess.streams {
64 if sess.streams[j].used {
65 f(&sess.streams[j])
66 }
67 }
68 }
69}
70
71// createSession answers CreateSession.
72func (s *Service) createSession(req *request) {
73 if req.remote {
74 s.replyErr(req, errNotSupported, "remote desktop sessions are not supported")
75 return
76 }
77 var sess *session
78 for i := range s.sessions {
79 if !s.sessions[i].used {
80 sess = &s.sessions[i]
81 break
82 }
83 }
84 if sess == nil {
85 s.replyErr(req, errLimits, "too many screen cast sessions")
86 return
87 }
88 s.nextSess++
89 *sess = session{used: true, id: s.nextSess, owner: req.sender}
90 sess.path = prefixSession + strconv.FormatUint(uint64(sess.id), 10)
91 s.log.Info().Str("sender", req.sender).Str("session", sess.path).Msg("screen cast session created")
92 if e := s.replyTo(req, "o"); e != nil {
93 e.ObjectPath(sess.path)
94 s.send()
95 }
96}
97
98// record answers RecordMonitor and RecordWindow: it adds a stream.
99func (s *Service) record(req *request, sess *session) {
100 if sess.started {
101 s.replyErr(req, errFailed, "session already started")
102 return
103 }
104 var st *stream
105 for i := range sess.streams {
106 if !sess.streams[i].used {
107 st = &sess.streams[i]
108 break
109 }
110 }
111 if st == nil {
112 s.replyErr(req, errLimits, "too many streams in the session")
113 return
114 }
115 *st = stream{used: true, sess: sess, cursor: req.cursor != 0, maxFPS: defaultFPS, pwState: -2}
116 if req.method == mRecordWindow {
117 if !s.haveWindow(ports.WindowID(req.window)) {
118 *st = stream{}
119 s.replyErr(req, errInvalidArgs, "no such window")
120 return
121 }
122 st.window = ports.WindowID(req.window)
123 // A window moves and resizes: the stream carries no position.
124 st.px, st.py, st.pw, st.ph = 0, 0, 1, 1
125 } else {
126 o := s.outputByName(req.text)
127 if o == nil {
128 *st = stream{}
129 s.replyErr(req, errInvalidArgs, "no such monitor")
130 return
131 }
132 st.output = o.Info.Name
133 st.px, st.py, st.pw, st.ph = int32(o.X), int32(o.Y), int32(o.Width), int32(o.Height)
134 if o.Current >= 0 && o.Current < len(o.Modes) {
135 if hz := o.Modes[o.Current].RefreshMilli / 1000; hz > 0 {
136 st.maxFPS = min(hz, 240)
137 }
138 }
139 }
140 s.nextStrm++
141 s.nextCast++
142 st.id, st.cast = s.nextStrm, s.nextCast
143 st.path = prefixStream + strconv.FormatUint(uint64(st.id), 10)
144 st.h2 = streamHandler{st: st}
145 if e := s.replyTo(req, "o"); e != nil {
146 e.ObjectPath(st.path)
147 s.send()
148 }
149}
150
151// start answers Start: the streams are opened on the wayland side.
152func (s *Service) start(req *request, sess *session) {
153 if sess.started {
154 s.replyErr(req, errFailed, "session already started")
155 return
156 }
157 n := 0
158 for i := range sess.streams {
159 if sess.streams[i].used {
160 sess.streams[i].needOpen = true
161 n++
162 }
163 }
164 if n == 0 {
165 s.replyErr(req, errFailed, "session has no stream")
166 return
167 }
168 sess.started = true
169 s.reply(req)
170}
171
172// closeSession ends a session: its streams are closed on the wayland side
173// and, with signal set, the owner is told. The slot is freed by reap.
174func (s *Service) closeSession(sess *session, signal bool) {
175 if sess.dying {
176 return
177 }
178 sess.dying = true
179 for i := range sess.streams {
180 if st := &sess.streams[i]; st.used {
181 st.close()
182 if !st.opened && st.src == nil {
183 *st = stream{}
184 }
185 }
186 }
187 if signal {
188 s.emitTo(sess.owner, sess.path, ifaceSession, "Closed")
189 s.send()
190 }
191 s.log.Info().Str("session", sess.path).Msg("screen cast session closed")
192}
193
194// outputByName finds an output of the inventory by connector.
195func (s *Service) outputByName(name string) *ports.CastOutput {
196 for i := range s.inv.Outputs {
197 if s.inv.Outputs[i].Info.Name == name {
198 return &s.inv.Outputs[i]
199 }
200 }
201 return nil
202}
203
204func (s *Service) haveWindow(id ports.WindowID) bool {
205 for i := range s.inv.Windows {
206 if s.inv.Windows[i].ID == id {
207 return true
208 }
209 }
210 return false
211}
212
213// properties answers Properties.Get, GetAll and Set.
214func (s *Service) properties(req *request, sess *session, st *stream) {
215 if req.method == mSet {
216 s.replyErr(req, errPropertyReadOnly, "no writable property")
217 return
218 }
219 iface := ""
220 switch req.obj {
221 case objScreenCast:
222 iface = ifaceScreenCast
223 case objSession:
224 iface = ifaceSession
225 case objStream:
226 iface = ifaceStream
227 case objDisplayConfig:
228 iface = ifaceDisplayConfig
229 case objIntrospect:
230 iface = ifaceIntrospect
231 case objScreenshot:
232 iface = ifaceScreenshot
233 }
234 if req.text != iface && !(req.method == mGetAll && req.text == "") {
235 s.replyErr(req, errUnknownInterface, "no such interface")
236 return
237 }
238 if req.method == mGetAll {
239 e := s.replyTo(req, "a{sv}")
240 if e == nil {
241 return
242 }
243 a := e.BeginArray('{')
244 switch req.obj {
245 case objScreenCast:
246 e.Struct()
247 e.Str("Version")
248 e.Variant("i")
249 e.Int32(screenCastVersion)
250 case objStream:
251 e.Struct()
252 e.Str("Parameters")
253 e.Variant("a{sv}")
254 encodeParameters(e, st)
255 case objIntrospect:
256 e.Struct()
257 e.Str("version")
258 e.Variant("u")
259 e.Uint32(introspectVersion)
260 }
261 e.EndArray(a)
262 s.send()
263 return
264 }
265 switch {
266 case req.obj == objScreenCast && req.text2 == "Version":
267 if e := s.replyTo(req, "v"); e != nil {
268 e.Variant("i")
269 e.Int32(screenCastVersion)
270 s.send()
271 }
272 case req.obj == objStream && req.text2 == "Parameters":
273 if e := s.replyTo(req, "v"); e != nil {
274 e.Variant("a{sv}")
275 encodeParameters(e, st)
276 s.send()
277 }
278 case req.obj == objIntrospect && req.text2 == "version":
279 if e := s.replyTo(req, "v"); e != nil {
280 e.Variant("u")
281 e.Uint32(introspectVersion)
282 s.send()
283 }
284 default:
285 s.replyErr(req, errUnknownProperty, "no such property")
286 }
287}
288
289// Versions served. Introspect version 2 would make x-d-p-g read
290// AnimationsEnabled and ScreenSize, which are not served.
291const (
292 screenCastVersion = 4
293 introspectVersion = 1
294 defaultFPS = 60
295)
296
297func itoa(n int) string { return strconv.Itoa(n) }
298func ftoa(f float64) string { return strconv.FormatFloat(f, 'f', 3, 64) }