refactor(wayland): extract the shared capture target from capture sessions
- commit
- 2c2814d4accacf5acd336b468b8511c548b8d8f4
- author
- brice · · 2026-10-14 19:05 UTC
- committer
- brice · · 2026-10-14 21:39 UTC
- parent
- 8e85801
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +76 -31
M internal/adapters/wayland/imagecopy.go +76 -31
| @@ -180,7 +180,7 @@ func (m copyCaptureManager) CreateSession(r *ext.ExtImageCopyCaptureManagerV1, i | |||
| 180 | 180 | if src != nil { | |
| 181 | 181 | cs = s.captureSources[src.Resource] | |
| 182 | 182 | } | |
| 183 | - | state := &captureSession{s: s, src: cs, cursor: options&1 != 0} | |
| 183 | + | state := &captureSession{captureTarget: captureTarget{s: s, src: cs}, cursor: options&1 != 0} | |
| 184 | 184 | res, err := ext.NewExtImageCopyCaptureSessionV1(r.Client(), 1, id, state) | |
| 185 | 185 | if err != nil { | |
| 186 | 186 | return | |
| @@ -243,19 +243,26 @@ func (c *cursorCaptureSession) GetCaptureSession(_ *ext.ExtImageCopyCaptureCurso | |||
| 243 | 243 | } | |
| 244 | 244 | } | |
| 245 | 245 | ||
| 246 | + | // captureTarget is what every registered capture follows, whatever consumes | |
| 247 | + | // its frames (an ext_image_copy_capture_session_v1 or a screencast cast): the | |
| 248 | + | // source, the id core knows it by and core's last decision about it. | |
| 249 | + | type captureTarget struct { | |
| 250 | + | s *Server | |
| 251 | + | src *captureSource | |
| 252 | + | id uint64 // registered with core; 0 when never | |
| 253 | + | // st is the last decision of core; haveState tells there is one. | |
| 254 | + | st ports.CaptureSessionState | |
| 255 | + | haveState bool | |
| 256 | + | } | |
| 257 | + | ||
| 246 | 258 | // captureSession is one ext_image_copy_capture_session_v1. | |
| 247 | 259 | type captureSession struct { | |
| 248 | - | s *Server | |
| 260 | + | captureTarget | |
| 249 | 261 | res *ext.ExtImageCopyCaptureSessionV1 | |
| 250 | - | src *captureSource | |
| 251 | - | id uint64 // registered with core; 0 when never | |
| 252 | 262 | cursor, stopped bool | |
| 253 | 263 | frame *captureExtFrame | |
| 254 | 264 | sent image.Point // buffer size of the last constraints | |
| 255 | - | // st is the last decision of core; haveState tells there is one. | |
| 256 | - | st ports.CaptureSessionState | |
| 257 | - | haveState bool | |
| 258 | - | excl *exclusion // the live exclusion object of this session | |
| 265 | + | excl *exclusion // the live exclusion object of this session | |
| 259 | 266 | // exclSeen: the last state of core knew an exclusion. Frames of an | |
| 260 | 267 | // excluded session leave the HUD out while it is (see tag). | |
| 261 | 268 | exclSeen bool | |
| @@ -290,7 +297,7 @@ func (c *captureSession) end(send, notify bool) { | |||
| 290 | 297 | // image of its off-screen render, from the origin) and whether the workspace | |
| 291 | 298 | // is off screen. Core's last state decides where a workspace is; before it, | |
| 292 | 299 | // the last workspace snapshot does. | |
| 293 | - | func (c *captureSession) geometry() (o *output, rect image.Rectangle, hidden, ok bool) { | |
| 300 | + | func (c *captureTarget) geometry() (o *output, rect image.Rectangle, hidden, ok bool) { | |
| 294 | 301 | src := c.src | |
| 295 | 302 | if src == nil { | |
| 296 | 303 | return nil, image.Rectangle{}, false, false | |
| @@ -377,24 +384,57 @@ func (c *captureSession) sendConstraints() { | |||
| 377 | 384 | c.res.SendDone() | |
| 378 | 385 | } | |
| 379 | 386 | ||
| 380 | - | // refresh follows the target: new constraints when the buffer size changed, | |
| 381 | - | // the end when nothing is left of it. | |
| 382 | - | func (c *captureSession) refresh() { | |
| 383 | - | if c.stopped || c.id == 0 { | |
| 384 | - | return | |
| 385 | - | } | |
| 387 | + | // targetFate says what became of a followed target. | |
| 388 | + | type targetFate uint8 | |
| 389 | + | ||
| 390 | + | const ( | |
| 391 | + | targetLive targetFate = iota | |
| 392 | + | targetWorkspaceGone // the workspace left the snapshot | |
| 393 | + | targetEmpty // an output or region with nothing left | |
| 394 | + | ) | |
| 395 | + | ||
| 396 | + | // follow looks at the target now: its fate and, while it lives, its geometry | |
| 397 | + | // (rect, ok as geometry gives them). A window or workspace that is not | |
| 398 | + | // locatable for the moment stays live: core's next state decides. | |
| 399 | + | func (c *captureTarget) follow() (fate targetFate, rect image.Rectangle, ok bool) { | |
| 386 | 400 | if c.src.kind == srcWorkspace { | |
| 387 | 401 | // The workspace left the snapshot: its handle is removed, stop at once | |
| 388 | 402 | // without waiting for core's state. | |
| 389 | 403 | if _, _, found := c.s.workspaceInfo(c.src.workspace); !found { | |
| 390 | - | c.s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWorkspaceGone)).Msg("capture session end") | |
| 391 | - | c.end(true, true) | |
| 392 | - | return | |
| 404 | + | return targetWorkspaceGone, image.Rectangle{}, false | |
| 393 | 405 | } | |
| 394 | 406 | } | |
| 395 | - | _, rect, _, ok := c.geometry() | |
| 407 | + | _, rect, _, ok = c.geometry() | |
| 408 | + | if !ok && c.src.kind != srcWorkspace && c.src.kind != srcWindow { | |
| 409 | + | return targetEmpty, rect, false | |
| 410 | + | } | |
| 411 | + | return targetLive, rect, ok | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | // applyState takes what core decided; it reports a decision that ends the | |
| 415 | + | // target (a terminal reason). | |
| 416 | + | func (c *captureTarget) applyState(st ports.CaptureSessionState) (ended bool) { | |
| 417 | + | if st.Reason.Terminal() { | |
| 418 | + | c.s.log.Info().Uint64("session", c.id).Str("reason", string(st.Reason)).Msg("capture session end") | |
| 419 | + | return true | |
| 420 | + | } | |
| 421 | + | c.st, c.haveState = st, true | |
| 422 | + | c.s.log.Debug().Uint64("session", c.id).Bool("active", st.Active).Bool("hidden", st.Hidden).Uint64("revision", st.Revision).Msg("capture session state") | |
| 423 | + | return false | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | // refresh follows the target: new constraints when the buffer size changed, | |
| 427 | + | // the end when nothing is left of it. | |
| 428 | + | func (c *captureSession) refresh() { | |
| 429 | + | if c.stopped || c.id == 0 { | |
| 430 | + | return | |
| 431 | + | } | |
| 432 | + | fate, rect, ok := c.follow() | |
| 396 | 433 | switch { | |
| 397 | - | case !ok && c.src.kind != srcWorkspace && c.src.kind != srcWindow: | |
| 434 | + | case fate == targetWorkspaceGone: | |
| 435 | + | c.s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWorkspaceGone)).Msg("capture session end") | |
| 436 | + | c.end(true, true) | |
| 437 | + | case fate == targetEmpty: | |
| 398 | 438 | c.s.log.Info().Uint64("session", c.id).Msg("capture session end: target empty") | |
| 399 | 439 | c.end(true, true) | |
| 400 | 440 | case ok && image.Pt(rect.Dx(), rect.Dy()) != c.sent: | |
| @@ -438,13 +478,11 @@ func (s *Server) captureState(st ports.CaptureSessionState) { | |||
| 438 | 478 | if c == nil { | |
| 439 | 479 | return | |
| 440 | 480 | } | |
| 441 | - | if st.Reason.Terminal() { | |
| 442 | - | s.log.Info().Uint64("session", c.id).Str("reason", string(st.Reason)).Msg("capture session end") | |
| 481 | + | if c.applyState(st) { | |
| 443 | 482 | c.end(true, false) | |
| 444 | 483 | return | |
| 445 | 484 | } | |
| 446 | - | c.st, c.haveState, c.exclSeen = st, true, st.Exclusion | |
| 447 | - | s.log.Debug().Uint64("session", c.id).Bool("active", st.Active).Bool("hidden", st.Hidden).Uint64("revision", st.Revision).Msg("capture session state") | |
| 485 | + | c.exclSeen = st.Exclusion | |
| 448 | 486 | if ex := c.excl; ex != nil { | |
| 449 | 487 | ex.confirm(st.Layers) | |
| 450 | 488 | } | |
| @@ -457,6 +495,19 @@ func (s *Server) captureState(st ports.CaptureSessionState) { | |||
| 457 | 495 | // still in the last state, a frame is not served rather than shown with the | |
| 458 | 496 | // HUD in it (ok false). | |
| 459 | 497 | func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) { | |
| 498 | + | t = c.targetTag(hidden) | |
| 499 | + | if c.excl != nil && !c.exclSeen { | |
| 500 | + | return t, false | |
| 501 | + | } | |
| 502 | + | if c.exclSeen { | |
| 503 | + | t.exclude, t.revision = true, c.st.Revision | |
| 504 | + | } | |
| 505 | + | return t, true | |
| 506 | + | } | |
| 507 | + | ||
| 508 | + | // targetTag is the part of a frame's tag the target decides: the session, | |
| 509 | + | // what is captured and what core is told of it. | |
| 510 | + | func (c *captureTarget) targetTag(hidden bool) (t captureTag) { | |
| 460 | 511 | t = captureTag{session: c.id} | |
| 461 | 512 | switch c.src.kind { | |
| 462 | 513 | case srcWorkspace: | |
| @@ -475,13 +526,7 @@ func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) { | |||
| 475 | 526 | case srcWindow: | |
| 476 | 527 | t.taken = ports.CaptureFrameTaken{Window: c.src.window} | |
| 477 | 528 | } | |
| 478 | - | if c.excl != nil && !c.exclSeen { | |
| 479 | - | return t, false | |
| 480 | - | } | |
| 481 | - | if c.exclSeen { | |
| 482 | - | t.exclude, t.revision = true, c.st.Revision | |
| 483 | - | } | |
| 484 | - | return t, true | |
| 529 | + | return t | |
| 485 | 530 | } | |
| 486 | 531 | ||
| 487 | 532 | func (c *captureSession) CreateFrame(r *ext.ExtImageCopyCaptureSessionV1, id uint32) { | |
| @@ -180,7 +180,7 @@ func (m copyCaptureManager) CreateSession(r *ext.ExtImageCopyCaptureManagerV1, i | |||||
| 180 | if src != nil { | 180 | if src != nil { | ||
| 181 | cs = s.captureSources[src.Resource] | 181 | cs = s.captureSources[src.Resource] | ||
| 182 | } | 182 | } | ||
| 183 | - | state := &captureSession{s: s, src: cs, cursor: options&1 != 0} | 183 | + | state := &captureSession{captureTarget: captureTarget{s: s, src: cs}, cursor: options&1 != 0} |
| 184 | res, err := ext.NewExtImageCopyCaptureSessionV1(r.Client(), 1, id, state) | 184 | res, err := ext.NewExtImageCopyCaptureSessionV1(r.Client(), 1, id, state) | ||
| 185 | if err != nil { | 185 | if err != nil { | ||
| 186 | return | 186 | return | ||
| @@ -243,19 +243,26 @@ func (c *cursorCaptureSession) GetCaptureSession(_ *ext.ExtImageCopyCaptureCurso | |||||
| 243 | } | 243 | } | ||
| 244 | } | 244 | } | ||
| 245 | 245 | ||||
| 246 | + | // captureTarget is what every registered capture follows, whatever consumes | |||
| 247 | + | // its frames (an ext_image_copy_capture_session_v1 or a screencast cast): the | |||
| 248 | + | // source, the id core knows it by and core's last decision about it. | |||
| 249 | + | type captureTarget struct { | |||
| 250 | + | s *Server | |||
| 251 | + | src *captureSource | |||
| 252 | + | id uint64 // registered with core; 0 when never | |||
| 253 | + | // st is the last decision of core; haveState tells there is one. | |||
| 254 | + | st ports.CaptureSessionState | |||
| 255 | + | haveState bool | |||
| 256 | + | } | |||
| 257 | + | ||||
| 246 | // captureSession is one ext_image_copy_capture_session_v1. | 258 | // captureSession is one ext_image_copy_capture_session_v1. | ||
| 247 | type captureSession struct { | 259 | type captureSession struct { | ||
| 248 | - | s *Server | 260 | + | captureTarget |
| 249 | res *ext.ExtImageCopyCaptureSessionV1 | 261 | res *ext.ExtImageCopyCaptureSessionV1 | ||
| 250 | - | src *captureSource | |||
| 251 | - | id uint64 // registered with core; 0 when never | |||
| 252 | cursor, stopped bool | 262 | cursor, stopped bool | ||
| 253 | frame *captureExtFrame | 263 | frame *captureExtFrame | ||
| 254 | sent image.Point // buffer size of the last constraints | 264 | sent image.Point // buffer size of the last constraints | ||
| 255 | - | // st is the last decision of core; haveState tells there is one. | 265 | + | excl *exclusion // the live exclusion object of this session |
| 256 | - | st ports.CaptureSessionState | |||
| 257 | - | haveState bool | |||
| 258 | - | excl *exclusion // the live exclusion object of this session | |||
| 259 | // exclSeen: the last state of core knew an exclusion. Frames of an | 266 | // exclSeen: the last state of core knew an exclusion. Frames of an | ||
| 260 | // excluded session leave the HUD out while it is (see tag). | 267 | // excluded session leave the HUD out while it is (see tag). | ||
| 261 | exclSeen bool | 268 | exclSeen bool | ||
| @@ -290,7 +297,7 @@ func (c *captureSession) end(send, notify bool) { | |||||
| 290 | // image of its off-screen render, from the origin) and whether the workspace | 297 | // image of its off-screen render, from the origin) and whether the workspace | ||
| 291 | // is off screen. Core's last state decides where a workspace is; before it, | 298 | // is off screen. Core's last state decides where a workspace is; before it, | ||
| 292 | // the last workspace snapshot does. | 299 | // the last workspace snapshot does. | ||
| 293 | - | func (c *captureSession) geometry() (o *output, rect image.Rectangle, hidden, ok bool) { | 300 | + | func (c *captureTarget) geometry() (o *output, rect image.Rectangle, hidden, ok bool) { |
| 294 | src := c.src | 301 | src := c.src | ||
| 295 | if src == nil { | 302 | if src == nil { | ||
| 296 | return nil, image.Rectangle{}, false, false | 303 | return nil, image.Rectangle{}, false, false | ||
| @@ -377,24 +384,57 @@ func (c *captureSession) sendConstraints() { | |||||
| 377 | c.res.SendDone() | 384 | c.res.SendDone() | ||
| 378 | } | 385 | } | ||
| 379 | 386 | ||||
| 380 | - | // refresh follows the target: new constraints when the buffer size changed, | 387 | + | // targetFate says what became of a followed target. |
| 381 | - | // the end when nothing is left of it. | 388 | + | type targetFate uint8 |
| 382 | - | func (c *captureSession) refresh() { | 389 | + | |
| 383 | - | if c.stopped || c.id == 0 { | 390 | + | const ( |
| 384 | - | return | 391 | + | targetLive targetFate = iota |
| 385 | - | } | 392 | + | targetWorkspaceGone // the workspace left the snapshot |
| 393 | + | targetEmpty // an output or region with nothing left | |||
| 394 | + | ) | |||
| 395 | + | ||||
| 396 | + | // follow looks at the target now: its fate and, while it lives, its geometry | |||
| 397 | + | // (rect, ok as geometry gives them). A window or workspace that is not | |||
| 398 | + | // locatable for the moment stays live: core's next state decides. | |||
| 399 | + | func (c *captureTarget) follow() (fate targetFate, rect image.Rectangle, ok bool) { | |||
| 386 | if c.src.kind == srcWorkspace { | 400 | if c.src.kind == srcWorkspace { | ||
| 387 | // The workspace left the snapshot: its handle is removed, stop at once | 401 | // The workspace left the snapshot: its handle is removed, stop at once | ||
| 388 | // without waiting for core's state. | 402 | // without waiting for core's state. | ||
| 389 | if _, _, found := c.s.workspaceInfo(c.src.workspace); !found { | 403 | if _, _, found := c.s.workspaceInfo(c.src.workspace); !found { | ||
| 390 | - | c.s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWorkspaceGone)).Msg("capture session end") | 404 | + | return targetWorkspaceGone, image.Rectangle{}, false |
| 391 | - | c.end(true, true) | |||
| 392 | - | return | |||
| 393 | } | 405 | } | ||
| 394 | } | 406 | } | ||
| 395 | - | _, rect, _, ok := c.geometry() | 407 | + | _, rect, _, ok = c.geometry() |
| 408 | + | if !ok && c.src.kind != srcWorkspace && c.src.kind != srcWindow { | |||
| 409 | + | return targetEmpty, rect, false | |||
| 410 | + | } | |||
| 411 | + | return targetLive, rect, ok | |||
| 412 | + | } | |||
| 413 | + | ||||
| 414 | + | // applyState takes what core decided; it reports a decision that ends the | |||
| 415 | + | // target (a terminal reason). | |||
| 416 | + | func (c *captureTarget) applyState(st ports.CaptureSessionState) (ended bool) { | |||
| 417 | + | if st.Reason.Terminal() { | |||
| 418 | + | c.s.log.Info().Uint64("session", c.id).Str("reason", string(st.Reason)).Msg("capture session end") | |||
| 419 | + | return true | |||
| 420 | + | } | |||
| 421 | + | c.st, c.haveState = st, true | |||
| 422 | + | c.s.log.Debug().Uint64("session", c.id).Bool("active", st.Active).Bool("hidden", st.Hidden).Uint64("revision", st.Revision).Msg("capture session state") | |||
| 423 | + | return false | |||
| 424 | + | } | |||
| 425 | + | ||||
| 426 | + | // refresh follows the target: new constraints when the buffer size changed, | |||
| 427 | + | // the end when nothing is left of it. | |||
| 428 | + | func (c *captureSession) refresh() { | |||
| 429 | + | if c.stopped || c.id == 0 { | |||
| 430 | + | return | |||
| 431 | + | } | |||
| 432 | + | fate, rect, ok := c.follow() | |||
| 396 | switch { | 433 | switch { | ||
| 397 | - | case !ok && c.src.kind != srcWorkspace && c.src.kind != srcWindow: | 434 | + | case fate == targetWorkspaceGone: |
| 435 | + | c.s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWorkspaceGone)).Msg("capture session end") | |||
| 436 | + | c.end(true, true) | |||
| 437 | + | case fate == targetEmpty: | |||
| 398 | c.s.log.Info().Uint64("session", c.id).Msg("capture session end: target empty") | 438 | c.s.log.Info().Uint64("session", c.id).Msg("capture session end: target empty") | ||
| 399 | c.end(true, true) | 439 | c.end(true, true) | ||
| 400 | case ok && image.Pt(rect.Dx(), rect.Dy()) != c.sent: | 440 | case ok && image.Pt(rect.Dx(), rect.Dy()) != c.sent: | ||
| @@ -438,13 +478,11 @@ func (s *Server) captureState(st ports.CaptureSessionState) { | |||||
| 438 | if c == nil { | 478 | if c == nil { | ||
| 439 | return | 479 | return | ||
| 440 | } | 480 | } | ||
| 441 | - | if st.Reason.Terminal() { | 481 | + | if c.applyState(st) { |
| 442 | - | s.log.Info().Uint64("session", c.id).Str("reason", string(st.Reason)).Msg("capture session end") | |||
| 443 | c.end(true, false) | 482 | c.end(true, false) | ||
| 444 | return | 483 | return | ||
| 445 | } | 484 | } | ||
| 446 | - | c.st, c.haveState, c.exclSeen = st, true, st.Exclusion | 485 | + | c.exclSeen = st.Exclusion |
| 447 | - | s.log.Debug().Uint64("session", c.id).Bool("active", st.Active).Bool("hidden", st.Hidden).Uint64("revision", st.Revision).Msg("capture session state") | |||
| 448 | if ex := c.excl; ex != nil { | 486 | if ex := c.excl; ex != nil { | ||
| 449 | ex.confirm(st.Layers) | 487 | ex.confirm(st.Layers) | ||
| 450 | } | 488 | } | ||
| @@ -457,6 +495,19 @@ func (s *Server) captureState(st ports.CaptureSessionState) { | |||||
| 457 | // still in the last state, a frame is not served rather than shown with the | 495 | // still in the last state, a frame is not served rather than shown with the | ||
| 458 | // HUD in it (ok false). | 496 | // HUD in it (ok false). | ||
| 459 | func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) { | 497 | func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) { | ||
| 498 | + | t = c.targetTag(hidden) | |||
| 499 | + | if c.excl != nil && !c.exclSeen { | |||
| 500 | + | return t, false | |||
| 501 | + | } | |||
| 502 | + | if c.exclSeen { | |||
| 503 | + | t.exclude, t.revision = true, c.st.Revision | |||
| 504 | + | } | |||
| 505 | + | return t, true | |||
| 506 | + | } | |||
| 507 | + | ||||
| 508 | + | // targetTag is the part of a frame's tag the target decides: the session, | |||
| 509 | + | // what is captured and what core is told of it. | |||
| 510 | + | func (c *captureTarget) targetTag(hidden bool) (t captureTag) { | |||
| 460 | t = captureTag{session: c.id} | 511 | t = captureTag{session: c.id} | ||
| 461 | switch c.src.kind { | 512 | switch c.src.kind { | ||
| 462 | case srcWorkspace: | 513 | case srcWorkspace: | ||
| @@ -475,13 +526,7 @@ func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) { | |||||
| 475 | case srcWindow: | 526 | case srcWindow: | ||
| 476 | t.taken = ports.CaptureFrameTaken{Window: c.src.window} | 527 | t.taken = ports.CaptureFrameTaken{Window: c.src.window} | ||
| 477 | } | 528 | } | ||
| 478 | - | if c.excl != nil && !c.exclSeen { | 529 | + | return t |
| 479 | - | return t, false | |||
| 480 | - | } | |||
| 481 | - | if c.exclSeen { | |||
| 482 | - | t.exclude, t.revision = true, c.st.Revision | |||
| 483 | - | } | |||
| 484 | - | return t, true | |||
| 485 | } | 530 | } | ||
| 486 | 531 | ||||
| 487 | func (c *captureSession) CreateFrame(r *ext.ExtImageCopyCaptureSessionV1, id uint32) { | 532 | func (c *captureSession) CreateFrame(r *ext.ExtImageCopyCaptureSessionV1, id uint32) { | ||