Merge pull request #21 from bnema/fix/idle-inhibition-cleanup
fix: prevent idle inhibition leak on pane close and add audio-based detection
5 files · +193 -17
M CHANGELOG.md +6 -2
| @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. | |||
| 4 | 4 | ||
| 5 | 5 | ## [Unreleased] | |
| 6 | 6 | ||
| 7 | + | ### Fixed | |
| 8 | + | - **Idle inhibition leak on pane close**: Fixed D-Bus idle inhibit request not being released when closing a pane/tab while in fullscreen or playing audio. The inhibition would remain active until the app exited. Now properly tracks fullscreen and audio state and releases inhibition before destroying WebView. | |
| 9 | + | - **Audio-based idle inhibition**: Extended idle inhibition to also activate when a page is playing audio (e.g., video/music playback), not just in fullscreen mode. Uses WebKit's `notify::is-playing-audio` signal. The inhibitor uses refcounting, so both fullscreen and audio can be active simultaneously. | |
| 10 | + | ||
| 11 | + | ## [0.20.1] - 2025-12-25 | |
| 12 | + | ||
| 7 | 13 | ### Fixed | |
| 8 | 14 | - **Stacked pane favicon and title updates**: Fixed favicon and title not updating in stacked pane title bars by tracking pane IDs in StackedView and properly routing update events to the correct pane. | |
| 9 | 15 | - **WebKit process leak on pane close**: Fixed zombie WebKit web processes not being terminated when panes were closed. `WebView.Destroy()` now properly calls `TerminateWebProcess()` and unparents GTK widgets, freeing GPU resources (VA-API, DMA-BUF, GL contexts). This should significantly improve video playback stability and reduce memory usage when opening/closing many panes. | |
| @@ -29,8 +35,6 @@ All notable changes to this project will be documented in this file. | |||
| 29 | 35 | - **New CLI commands**: `doctor` (runtime dependency checks), `about` (build info), `setup` (desktop integration), plus expanded `purge` with an interactive TUI. | |
| 30 | 36 | - **WebUI config improvements**: Search shortcut management (CRUD) and reset-to-defaults support. | |
| 31 | 37 | - **Find-in-page**: New find UI and shortcut handling. | |
| 32 | - | ||
| 33 | - | ### Changed | |
| 34 | 38 | - Migrated GUI stack to `puregotk` + `puregotk-webkit` and disabled CGO for pure Go compilation. | |
| 35 | 39 | - **Omnibox rewrite**: Replaced injected Svelte/JS omnibox with a native GTK4 widget overlay. | |
| 36 | 40 | - Startup defers non-critical work, adds DNS prefetching and a warmed WebView pool for faster tab/pane creation. | |
| @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. | |||||
| 4 | 4 | ||||
| 5 | ## [Unreleased] | 5 | ## [Unreleased] | ||
| 6 | 6 | ||||
| 7 | + | ### Fixed | |||
| 8 | + | - **Idle inhibition leak on pane close**: Fixed D-Bus idle inhibit request not being released when closing a pane/tab while in fullscreen or playing audio. The inhibition would remain active until the app exited. Now properly tracks fullscreen and audio state and releases inhibition before destroying WebView. | |||
| 9 | + | - **Audio-based idle inhibition**: Extended idle inhibition to also activate when a page is playing audio (e.g., video/music playback), not just in fullscreen mode. Uses WebKit's `notify::is-playing-audio` signal. The inhibitor uses refcounting, so both fullscreen and audio can be active simultaneously. | |||
| 10 | + | ||||
| 11 | + | ## [0.20.1] - 2025-12-25 | |||
| 12 | + | ||||
| 7 | ### Fixed | 13 | ### Fixed | ||
| 8 | - **Stacked pane favicon and title updates**: Fixed favicon and title not updating in stacked pane title bars by tracking pane IDs in StackedView and properly routing update events to the correct pane. | 14 | - **Stacked pane favicon and title updates**: Fixed favicon and title not updating in stacked pane title bars by tracking pane IDs in StackedView and properly routing update events to the correct pane. | ||
| 9 | - **WebKit process leak on pane close**: Fixed zombie WebKit web processes not being terminated when panes were closed. `WebView.Destroy()` now properly calls `TerminateWebProcess()` and unparents GTK widgets, freeing GPU resources (VA-API, DMA-BUF, GL contexts). This should significantly improve video playback stability and reduce memory usage when opening/closing many panes. | 15 | - **WebKit process leak on pane close**: Fixed zombie WebKit web processes not being terminated when panes were closed. `WebView.Destroy()` now properly calls `TerminateWebProcess()` and unparents GTK widgets, freeing GPU resources (VA-API, DMA-BUF, GL contexts). This should significantly improve video playback stability and reduce memory usage when opening/closing many panes. | ||
| @@ -29,8 +35,6 @@ All notable changes to this project will be documented in this file. | |||||
| 29 | - **New CLI commands**: `doctor` (runtime dependency checks), `about` (build info), `setup` (desktop integration), plus expanded `purge` with an interactive TUI. | 35 | - **New CLI commands**: `doctor` (runtime dependency checks), `about` (build info), `setup` (desktop integration), plus expanded `purge` with an interactive TUI. | ||
| 30 | - **WebUI config improvements**: Search shortcut management (CRUD) and reset-to-defaults support. | 36 | - **WebUI config improvements**: Search shortcut management (CRUD) and reset-to-defaults support. | ||
| 31 | - **Find-in-page**: New find UI and shortcut handling. | 37 | - **Find-in-page**: New find UI and shortcut handling. | ||
| 32 | - | ||||
| 33 | - | ### Changed | |||
| 34 | - Migrated GUI stack to `puregotk` + `puregotk-webkit` and disabled CGO for pure Go compilation. | 38 | - Migrated GUI stack to `puregotk` + `puregotk-webkit` and disabled CGO for pure Go compilation. | ||
| 35 | - **Omnibox rewrite**: Replaced injected Svelte/JS omnibox with a native GTK4 widget overlay. | 39 | - **Omnibox rewrite**: Replaced injected Svelte/JS omnibox with a native GTK4 widget overlay. | ||
| 36 | - Startup defers non-critical work, adds DNS prefetching and a warmed WebView pool for faster tab/pane creation. | 40 | - Startup defers non-critical work, adds DNS prefetching and a warmed WebView pool for faster tab/pane creation. | ||
M internal/application/port/mocks/mocks.go +88 -0
| @@ -592,6 +592,50 @@ func (_c *MockWebView_IsDestroyed_Call) RunAndReturn(run func() bool) *MockWebVi | |||
| 592 | 592 | return _c | |
| 593 | 593 | } | |
| 594 | 594 | ||
| 595 | + | // IsFullscreen provides a mock function for the type MockWebView | |
| 596 | + | func (_mock *MockWebView) IsFullscreen() bool { | |
| 597 | + | ret := _mock.Called() | |
| 598 | + | ||
| 599 | + | if len(ret) == 0 { | |
| 600 | + | panic("no return value specified for IsFullscreen") | |
| 601 | + | } | |
| 602 | + | ||
| 603 | + | var r0 bool | |
| 604 | + | if returnFunc, ok := ret.Get(0).(func() bool); ok { | |
| 605 | + | r0 = returnFunc() | |
| 606 | + | } else { | |
| 607 | + | r0 = ret.Get(0).(bool) | |
| 608 | + | } | |
| 609 | + | return r0 | |
| 610 | + | } | |
| 611 | + | ||
| 612 | + | // MockWebView_IsFullscreen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsFullscreen' | |
| 613 | + | type MockWebView_IsFullscreen_Call struct { | |
| 614 | + | *mock.Call | |
| 615 | + | } | |
| 616 | + | ||
| 617 | + | // IsFullscreen is a helper method to define mock.On call | |
| 618 | + | func (_e *MockWebView_Expecter) IsFullscreen() *MockWebView_IsFullscreen_Call { | |
| 619 | + | return &MockWebView_IsFullscreen_Call{Call: _e.mock.On("IsFullscreen")} | |
| 620 | + | } | |
| 621 | + | ||
| 622 | + | func (_c *MockWebView_IsFullscreen_Call) Run(run func()) *MockWebView_IsFullscreen_Call { | |
| 623 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 624 | + | run() | |
| 625 | + | }) | |
| 626 | + | return _c | |
| 627 | + | } | |
| 628 | + | ||
| 629 | + | func (_c *MockWebView_IsFullscreen_Call) Return(b bool) *MockWebView_IsFullscreen_Call { | |
| 630 | + | _c.Call.Return(b) | |
| 631 | + | return _c | |
| 632 | + | } | |
| 633 | + | ||
| 634 | + | func (_c *MockWebView_IsFullscreen_Call) RunAndReturn(run func() bool) *MockWebView_IsFullscreen_Call { | |
| 635 | + | _c.Call.Return(run) | |
| 636 | + | return _c | |
| 637 | + | } | |
| 638 | + | ||
| 595 | 639 | // IsLoading provides a mock function for the type MockWebView | |
| 596 | 640 | func (_mock *MockWebView) IsLoading() bool { | |
| 597 | 641 | ret := _mock.Called() | |
| @@ -636,6 +680,50 @@ func (_c *MockWebView_IsLoading_Call) RunAndReturn(run func() bool) *MockWebView | |||
| 636 | 680 | return _c | |
| 637 | 681 | } | |
| 638 | 682 | ||
| 683 | + | // IsPlayingAudio provides a mock function for the type MockWebView | |
| 684 | + | func (_mock *MockWebView) IsPlayingAudio() bool { | |
| 685 | + | ret := _mock.Called() | |
| 686 | + | ||
| 687 | + | if len(ret) == 0 { | |
| 688 | + | panic("no return value specified for IsPlayingAudio") | |
| 689 | + | } | |
| 690 | + | ||
| 691 | + | var r0 bool | |
| 692 | + | if returnFunc, ok := ret.Get(0).(func() bool); ok { | |
| 693 | + | r0 = returnFunc() | |
| 694 | + | } else { | |
| 695 | + | r0 = ret.Get(0).(bool) | |
| 696 | + | } | |
| 697 | + | return r0 | |
| 698 | + | } | |
| 699 | + | ||
| 700 | + | // MockWebView_IsPlayingAudio_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsPlayingAudio' | |
| 701 | + | type MockWebView_IsPlayingAudio_Call struct { | |
| 702 | + | *mock.Call | |
| 703 | + | } | |
| 704 | + | ||
| 705 | + | // IsPlayingAudio is a helper method to define mock.On call | |
| 706 | + | func (_e *MockWebView_Expecter) IsPlayingAudio() *MockWebView_IsPlayingAudio_Call { | |
| 707 | + | return &MockWebView_IsPlayingAudio_Call{Call: _e.mock.On("IsPlayingAudio")} | |
| 708 | + | } | |
| 709 | + | ||
| 710 | + | func (_c *MockWebView_IsPlayingAudio_Call) Run(run func()) *MockWebView_IsPlayingAudio_Call { | |
| 711 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 712 | + | run() | |
| 713 | + | }) | |
| 714 | + | return _c | |
| 715 | + | } | |
| 716 | + | ||
| 717 | + | func (_c *MockWebView_IsPlayingAudio_Call) Return(b bool) *MockWebView_IsPlayingAudio_Call { | |
| 718 | + | _c.Call.Return(b) | |
| 719 | + | return _c | |
| 720 | + | } | |
| 721 | + | ||
| 722 | + | func (_c *MockWebView_IsPlayingAudio_Call) RunAndReturn(run func() bool) *MockWebView_IsPlayingAudio_Call { | |
| 723 | + | _c.Call.Return(run) | |
| 724 | + | return _c | |
| 725 | + | } | |
| 726 | + | ||
| 639 | 727 | // LoadHTML provides a mock function for the type MockWebView | |
| 640 | 728 | func (_mock *MockWebView) LoadHTML(ctx context.Context, content string, baseURI string) error { | |
| 641 | 729 | ret := _mock.Called(ctx, content, baseURI) | |
| @@ -592,6 +592,50 @@ func (_c *MockWebView_IsDestroyed_Call) RunAndReturn(run func() bool) *MockWebVi | |||||
| 592 | return _c | 592 | return _c | ||
| 593 | } | 593 | } | ||
| 594 | 594 | ||||
| 595 | + | // IsFullscreen provides a mock function for the type MockWebView | |||
| 596 | + | func (_mock *MockWebView) IsFullscreen() bool { | |||
| 597 | + | ret := _mock.Called() | |||
| 598 | + | ||||
| 599 | + | if len(ret) == 0 { | |||
| 600 | + | panic("no return value specified for IsFullscreen") | |||
| 601 | + | } | |||
| 602 | + | ||||
| 603 | + | var r0 bool | |||
| 604 | + | if returnFunc, ok := ret.Get(0).(func() bool); ok { | |||
| 605 | + | r0 = returnFunc() | |||
| 606 | + | } else { | |||
| 607 | + | r0 = ret.Get(0).(bool) | |||
| 608 | + | } | |||
| 609 | + | return r0 | |||
| 610 | + | } | |||
| 611 | + | ||||
| 612 | + | // MockWebView_IsFullscreen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsFullscreen' | |||
| 613 | + | type MockWebView_IsFullscreen_Call struct { | |||
| 614 | + | *mock.Call | |||
| 615 | + | } | |||
| 616 | + | ||||
| 617 | + | // IsFullscreen is a helper method to define mock.On call | |||
| 618 | + | func (_e *MockWebView_Expecter) IsFullscreen() *MockWebView_IsFullscreen_Call { | |||
| 619 | + | return &MockWebView_IsFullscreen_Call{Call: _e.mock.On("IsFullscreen")} | |||
| 620 | + | } | |||
| 621 | + | ||||
| 622 | + | func (_c *MockWebView_IsFullscreen_Call) Run(run func()) *MockWebView_IsFullscreen_Call { | |||
| 623 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 624 | + | run() | |||
| 625 | + | }) | |||
| 626 | + | return _c | |||
| 627 | + | } | |||
| 628 | + | ||||
| 629 | + | func (_c *MockWebView_IsFullscreen_Call) Return(b bool) *MockWebView_IsFullscreen_Call { | |||
| 630 | + | _c.Call.Return(b) | |||
| 631 | + | return _c | |||
| 632 | + | } | |||
| 633 | + | ||||
| 634 | + | func (_c *MockWebView_IsFullscreen_Call) RunAndReturn(run func() bool) *MockWebView_IsFullscreen_Call { | |||
| 635 | + | _c.Call.Return(run) | |||
| 636 | + | return _c | |||
| 637 | + | } | |||
| 638 | + | ||||
| 595 | // IsLoading provides a mock function for the type MockWebView | 639 | // IsLoading provides a mock function for the type MockWebView | ||
| 596 | func (_mock *MockWebView) IsLoading() bool { | 640 | func (_mock *MockWebView) IsLoading() bool { | ||
| 597 | ret := _mock.Called() | 641 | ret := _mock.Called() | ||
| @@ -636,6 +680,50 @@ func (_c *MockWebView_IsLoading_Call) RunAndReturn(run func() bool) *MockWebView | |||||
| 636 | return _c | 680 | return _c | ||
| 637 | } | 681 | } | ||
| 638 | 682 | ||||
| 683 | + | // IsPlayingAudio provides a mock function for the type MockWebView | |||
| 684 | + | func (_mock *MockWebView) IsPlayingAudio() bool { | |||
| 685 | + | ret := _mock.Called() | |||
| 686 | + | ||||
| 687 | + | if len(ret) == 0 { | |||
| 688 | + | panic("no return value specified for IsPlayingAudio") | |||
| 689 | + | } | |||
| 690 | + | ||||
| 691 | + | var r0 bool | |||
| 692 | + | if returnFunc, ok := ret.Get(0).(func() bool); ok { | |||
| 693 | + | r0 = returnFunc() | |||
| 694 | + | } else { | |||
| 695 | + | r0 = ret.Get(0).(bool) | |||
| 696 | + | } | |||
| 697 | + | return r0 | |||
| 698 | + | } | |||
| 699 | + | ||||
| 700 | + | // MockWebView_IsPlayingAudio_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsPlayingAudio' | |||
| 701 | + | type MockWebView_IsPlayingAudio_Call struct { | |||
| 702 | + | *mock.Call | |||
| 703 | + | } | |||
| 704 | + | ||||
| 705 | + | // IsPlayingAudio is a helper method to define mock.On call | |||
| 706 | + | func (_e *MockWebView_Expecter) IsPlayingAudio() *MockWebView_IsPlayingAudio_Call { | |||
| 707 | + | return &MockWebView_IsPlayingAudio_Call{Call: _e.mock.On("IsPlayingAudio")} | |||
| 708 | + | } | |||
| 709 | + | ||||
| 710 | + | func (_c *MockWebView_IsPlayingAudio_Call) Run(run func()) *MockWebView_IsPlayingAudio_Call { | |||
| 711 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 712 | + | run() | |||
| 713 | + | }) | |||
| 714 | + | return _c | |||
| 715 | + | } | |||
| 716 | + | ||||
| 717 | + | func (_c *MockWebView_IsPlayingAudio_Call) Return(b bool) *MockWebView_IsPlayingAudio_Call { | |||
| 718 | + | _c.Call.Return(b) | |||
| 719 | + | return _c | |||
| 720 | + | } | |||
| 721 | + | ||||
| 722 | + | func (_c *MockWebView_IsPlayingAudio_Call) RunAndReturn(run func() bool) *MockWebView_IsPlayingAudio_Call { | |||
| 723 | + | _c.Call.Return(run) | |||
| 724 | + | return _c | |||
| 725 | + | } | |||
| 726 | + | ||||
| 639 | // LoadHTML provides a mock function for the type MockWebView | 727 | // LoadHTML provides a mock function for the type MockWebView | ||
| 640 | func (_mock *MockWebView) LoadHTML(ctx context.Context, content string, baseURI string) error { | 728 | func (_mock *MockWebView) LoadHTML(ctx context.Context, content string, baseURI string) error { | ||
| 641 | ret := _mock.Called(ctx, content, baseURI) | 729 | ret := _mock.Called(ctx, content, baseURI) | ||
M internal/application/port/webview.go +8 -0
| @@ -190,6 +190,14 @@ type WebView interface { | |||
| 190 | 190 | // Pass nil to clear all callbacks. | |
| 191 | 191 | SetCallbacks(callbacks *WebViewCallbacks) | |
| 192 | 192 | ||
| 193 | + | // --- Media/Fullscreen State --- | |
| 194 | + | ||
| 195 | + | // IsFullscreen returns true if the WebView is currently in fullscreen mode. | |
| 196 | + | IsFullscreen() bool | |
| 197 | + | ||
| 198 | + | // IsPlayingAudio returns true if the WebView is currently playing audio. | |
| 199 | + | IsPlayingAudio() bool | |
| 200 | + | ||
| 193 | 201 | // --- Lifecycle --- | |
| 194 | 202 | ||
| 195 | 203 | // IsDestroyed returns true if the WebView has been destroyed. | |
| @@ -190,6 +190,14 @@ type WebView interface { | |||||
| 190 | // Pass nil to clear all callbacks. | 190 | // Pass nil to clear all callbacks. | ||
| 191 | SetCallbacks(callbacks *WebViewCallbacks) | 191 | SetCallbacks(callbacks *WebViewCallbacks) | ||
| 192 | 192 | ||||
| 193 | + | // --- Media/Fullscreen State --- | |||
| 194 | + | ||||
| 195 | + | // IsFullscreen returns true if the WebView is currently in fullscreen mode. | |||
| 196 | + | IsFullscreen() bool | |||
| 197 | + | ||||
| 198 | + | // IsPlayingAudio returns true if the WebView is currently playing audio. | |||
| 199 | + | IsPlayingAudio() bool | |||
| 200 | + | ||||
| 193 | // --- Lifecycle --- | 201 | // --- Lifecycle --- | ||
| 194 | 202 | ||||
| 195 | // IsDestroyed returns true if the WebView has been destroyed. | 203 | // IsDestroyed returns true if the WebView has been destroyed. | ||
M internal/infrastructure/webkit/webview.go +48 -11
| @@ -95,6 +95,10 @@ type WebView struct { | |||
| 95 | 95 | canGoFwd bool | |
| 96 | 96 | isLoading bool | |
| 97 | 97 | ||
| 98 | + | // Media/fullscreen state for idle inhibition cleanup | |
| 99 | + | isFullscreen atomic.Bool | |
| 100 | + | isPlayingAudio atomic.Bool | |
| 101 | + | ||
| 98 | 102 | // Progress throttling (~60fps) | |
| 99 | 103 | lastProgressUpdate atomic.Int64 // Unix nanoseconds | |
| 100 | 104 | ||
| @@ -102,17 +106,18 @@ type WebView struct { | |||
| 102 | 106 | signalIDs []uint32 | |
| 103 | 107 | ||
| 104 | 108 | // Callbacks (set by UI layer) | |
| 105 | - | OnLoadChanged func(LoadEvent) | |
| 106 | - | OnTitleChanged func(string) | |
| 107 | - | OnURIChanged func(string) | |
| 108 | - | OnProgressChanged func(float64) | |
| 109 | - | OnFaviconChanged func(*gdk.Texture) // Called when page favicon changes | |
| 110 | - | OnClose func() | |
| 111 | - | OnCreate func(PopupRequest) *WebView // Return new WebView or nil to block popup | |
| 112 | - | OnReadyToShow func() // Called when popup is ready to display | |
| 113 | - | OnLinkMiddleClick func(uri string) bool // Return true if handled (blocks navigation) | |
| 114 | - | OnEnterFullscreen func() bool // Return true to prevent fullscreen | |
| 115 | - | OnLeaveFullscreen func() bool // Return true to prevent leaving fullscreen | |
| 109 | + | OnLoadChanged func(LoadEvent) | |
| 110 | + | OnTitleChanged func(string) | |
| 111 | + | OnURIChanged func(string) | |
| 112 | + | OnProgressChanged func(float64) | |
| 113 | + | OnFaviconChanged func(*gdk.Texture) // Called when page favicon changes | |
| 114 | + | OnClose func() | |
| 115 | + | OnCreate func(PopupRequest) *WebView // Return new WebView or nil to block popup | |
| 116 | + | OnReadyToShow func() // Called when popup is ready to display | |
| 117 | + | OnLinkMiddleClick func(uri string) bool // Return true if handled (blocks navigation) | |
| 118 | + | OnEnterFullscreen func() bool // Return true to prevent fullscreen | |
| 119 | + | OnLeaveFullscreen func() bool // Return true to prevent leaving fullscreen | |
| 120 | + | OnAudioStateChanged func(playing bool) // Called when audio playback starts/stops | |
| 116 | 121 | ||
| 117 | 122 | logger zerolog.Logger | |
| 118 | 123 | mu sync.RWMutex | |
| @@ -322,6 +327,7 @@ func (wv *WebView) connectSignals() { | |||
| 322 | 327 | wv.connectDecidePolicySignal() | |
| 323 | 328 | wv.connectEnterFullscreenSignal() | |
| 324 | 329 | wv.connectLeaveFullscreenSignal() | |
| 330 | + | wv.connectAudioStateSignal() | |
| 325 | 331 | } | |
| 326 | 332 | ||
| 327 | 333 | func (wv *WebView) connectLoadChangedSignal() { | |
| @@ -545,6 +551,7 @@ func (wv *WebView) connectDecidePolicySignal() { | |||
| 545 | 551 | ||
| 546 | 552 | func (wv *WebView) connectEnterFullscreenSignal() { | |
| 547 | 553 | enterFullscreenCb := func(_ webkit.WebView) bool { | |
| 554 | + | wv.isFullscreen.Store(true) | |
| 548 | 555 | wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("enter fullscreen") | |
| 549 | 556 | if wv.OnEnterFullscreen != nil { | |
| 550 | 557 | return wv.OnEnterFullscreen() | |
| @@ -557,6 +564,7 @@ func (wv *WebView) connectEnterFullscreenSignal() { | |||
| 557 | 564 | ||
| 558 | 565 | func (wv *WebView) connectLeaveFullscreenSignal() { | |
| 559 | 566 | leaveFullscreenCb := func(_ webkit.WebView) bool { | |
| 567 | + | wv.isFullscreen.Store(false) | |
| 560 | 568 | wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("leave fullscreen") | |
| 561 | 569 | if wv.OnLeaveFullscreen != nil { | |
| 562 | 570 | return wv.OnLeaveFullscreen() | |
| @@ -567,6 +575,24 @@ func (wv *WebView) connectLeaveFullscreenSignal() { | |||
| 567 | 575 | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 568 | 576 | } | |
| 569 | 577 | ||
| 578 | + | func (wv *WebView) connectAudioStateSignal() { | |
| 579 | + | audioCb := func() { | |
| 580 | + | playing := wv.inner.IsPlayingAudio() | |
| 581 | + | oldState := wv.isPlayingAudio.Swap(playing) | |
| 582 | + | if oldState != playing { | |
| 583 | + | wv.logger.Debug(). | |
| 584 | + | Uint64("id", uint64(wv.id)). | |
| 585 | + | Bool("playing", playing). | |
| 586 | + | Msg("audio state changed") | |
| 587 | + | if wv.OnAudioStateChanged != nil { | |
| 588 | + | wv.OnAudioStateChanged(playing) | |
| 589 | + | } | |
| 590 | + | } | |
| 591 | + | } | |
| 592 | + | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::is-playing-audio", glib.NewCallback(&audioCb)) | |
| 593 | + | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 594 | + | } | |
| 595 | + | ||
| 570 | 596 | // ID returns the unique identifier for this WebView. | |
| 571 | 597 | func (wv *WebView) ID() WebViewID { | |
| 572 | 598 | return wv.id | |
| @@ -582,6 +608,16 @@ func (wv *WebView) Widget() *webkit.WebView { | |||
| 582 | 608 | return wv.inner | |
| 583 | 609 | } | |
| 584 | 610 | ||
| 611 | + | // IsFullscreen returns true if the WebView is currently in fullscreen mode. | |
| 612 | + | func (wv *WebView) IsFullscreen() bool { | |
| 613 | + | return wv.isFullscreen.Load() | |
| 614 | + | } | |
| 615 | + | ||
| 616 | + | // IsPlayingAudio returns true if the WebView is currently playing audio. | |
| 617 | + | func (wv *WebView) IsPlayingAudio() bool { | |
| 618 | + | return wv.isPlayingAudio.Load() | |
| 619 | + | } | |
| 620 | + | ||
| 585 | 621 | // GetFindController returns the WebKit FindController wrapped in the port interface. | |
| 586 | 622 | // The adapter is cached to prevent the Go wrapper from being garbage collected. | |
| 587 | 623 | func (wv *WebView) GetFindController() port.FindController { | |
| @@ -898,6 +934,7 @@ func (wv *WebView) Destroy() { | |||
| 898 | 934 | wv.OnLinkMiddleClick = nil | |
| 899 | 935 | wv.OnEnterFullscreen = nil | |
| 900 | 936 | wv.OnLeaveFullscreen = nil | |
| 937 | + | wv.OnAudioStateChanged = nil | |
| 901 | 938 | ||
| 902 | 939 | // 3. Clear async callback references | |
| 903 | 940 | wv.mu.Lock() | |
| @@ -95,6 +95,10 @@ type WebView struct { | |||||
| 95 | canGoFwd bool | 95 | canGoFwd bool | ||
| 96 | isLoading bool | 96 | isLoading bool | ||
| 97 | 97 | ||||
| 98 | + | // Media/fullscreen state for idle inhibition cleanup | |||
| 99 | + | isFullscreen atomic.Bool | |||
| 100 | + | isPlayingAudio atomic.Bool | |||
| 101 | + | ||||
| 98 | // Progress throttling (~60fps) | 102 | // Progress throttling (~60fps) | ||
| 99 | lastProgressUpdate atomic.Int64 // Unix nanoseconds | 103 | lastProgressUpdate atomic.Int64 // Unix nanoseconds | ||
| 100 | 104 | ||||
| @@ -102,17 +106,18 @@ type WebView struct { | |||||
| 102 | signalIDs []uint32 | 106 | signalIDs []uint32 | ||
| 103 | 107 | ||||
| 104 | // Callbacks (set by UI layer) | 108 | // Callbacks (set by UI layer) | ||
| 105 | - | OnLoadChanged func(LoadEvent) | 109 | + | OnLoadChanged func(LoadEvent) |
| 106 | - | OnTitleChanged func(string) | 110 | + | OnTitleChanged func(string) |
| 107 | - | OnURIChanged func(string) | 111 | + | OnURIChanged func(string) |
| 108 | - | OnProgressChanged func(float64) | 112 | + | OnProgressChanged func(float64) |
| 109 | - | OnFaviconChanged func(*gdk.Texture) // Called when page favicon changes | 113 | + | OnFaviconChanged func(*gdk.Texture) // Called when page favicon changes |
| 110 | - | OnClose func() | 114 | + | OnClose func() |
| 111 | - | OnCreate func(PopupRequest) *WebView // Return new WebView or nil to block popup | 115 | + | OnCreate func(PopupRequest) *WebView // Return new WebView or nil to block popup |
| 112 | - | OnReadyToShow func() // Called when popup is ready to display | 116 | + | OnReadyToShow func() // Called when popup is ready to display |
| 113 | - | OnLinkMiddleClick func(uri string) bool // Return true if handled (blocks navigation) | 117 | + | OnLinkMiddleClick func(uri string) bool // Return true if handled (blocks navigation) |
| 114 | - | OnEnterFullscreen func() bool // Return true to prevent fullscreen | 118 | + | OnEnterFullscreen func() bool // Return true to prevent fullscreen |
| 115 | - | OnLeaveFullscreen func() bool // Return true to prevent leaving fullscreen | 119 | + | OnLeaveFullscreen func() bool // Return true to prevent leaving fullscreen |
| 120 | + | OnAudioStateChanged func(playing bool) // Called when audio playback starts/stops | |||
| 116 | 121 | ||||
| 117 | logger zerolog.Logger | 122 | logger zerolog.Logger | ||
| 118 | mu sync.RWMutex | 123 | mu sync.RWMutex | ||
| @@ -322,6 +327,7 @@ func (wv *WebView) connectSignals() { | |||||
| 322 | wv.connectDecidePolicySignal() | 327 | wv.connectDecidePolicySignal() | ||
| 323 | wv.connectEnterFullscreenSignal() | 328 | wv.connectEnterFullscreenSignal() | ||
| 324 | wv.connectLeaveFullscreenSignal() | 329 | wv.connectLeaveFullscreenSignal() | ||
| 330 | + | wv.connectAudioStateSignal() | |||
| 325 | } | 331 | } | ||
| 326 | 332 | ||||
| 327 | func (wv *WebView) connectLoadChangedSignal() { | 333 | func (wv *WebView) connectLoadChangedSignal() { | ||
| @@ -545,6 +551,7 @@ func (wv *WebView) connectDecidePolicySignal() { | |||||
| 545 | 551 | ||||
| 546 | func (wv *WebView) connectEnterFullscreenSignal() { | 552 | func (wv *WebView) connectEnterFullscreenSignal() { | ||
| 547 | enterFullscreenCb := func(_ webkit.WebView) bool { | 553 | enterFullscreenCb := func(_ webkit.WebView) bool { | ||
| 554 | + | wv.isFullscreen.Store(true) | |||
| 548 | wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("enter fullscreen") | 555 | wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("enter fullscreen") | ||
| 549 | if wv.OnEnterFullscreen != nil { | 556 | if wv.OnEnterFullscreen != nil { | ||
| 550 | return wv.OnEnterFullscreen() | 557 | return wv.OnEnterFullscreen() | ||
| @@ -557,6 +564,7 @@ func (wv *WebView) connectEnterFullscreenSignal() { | |||||
| 557 | 564 | ||||
| 558 | func (wv *WebView) connectLeaveFullscreenSignal() { | 565 | func (wv *WebView) connectLeaveFullscreenSignal() { | ||
| 559 | leaveFullscreenCb := func(_ webkit.WebView) bool { | 566 | leaveFullscreenCb := func(_ webkit.WebView) bool { | ||
| 567 | + | wv.isFullscreen.Store(false) | |||
| 560 | wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("leave fullscreen") | 568 | wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("leave fullscreen") | ||
| 561 | if wv.OnLeaveFullscreen != nil { | 569 | if wv.OnLeaveFullscreen != nil { | ||
| 562 | return wv.OnLeaveFullscreen() | 570 | return wv.OnLeaveFullscreen() | ||
| @@ -567,6 +575,24 @@ func (wv *WebView) connectLeaveFullscreenSignal() { | |||||
| 567 | wv.signalIDs = append(wv.signalIDs, sigID) | 575 | wv.signalIDs = append(wv.signalIDs, sigID) | ||
| 568 | } | 576 | } | ||
| 569 | 577 | ||||
| 578 | + | func (wv *WebView) connectAudioStateSignal() { | |||
| 579 | + | audioCb := func() { | |||
| 580 | + | playing := wv.inner.IsPlayingAudio() | |||
| 581 | + | oldState := wv.isPlayingAudio.Swap(playing) | |||
| 582 | + | if oldState != playing { | |||
| 583 | + | wv.logger.Debug(). | |||
| 584 | + | Uint64("id", uint64(wv.id)). | |||
| 585 | + | Bool("playing", playing). | |||
| 586 | + | Msg("audio state changed") | |||
| 587 | + | if wv.OnAudioStateChanged != nil { | |||
| 588 | + | wv.OnAudioStateChanged(playing) | |||
| 589 | + | } | |||
| 590 | + | } | |||
| 591 | + | } | |||
| 592 | + | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::is-playing-audio", glib.NewCallback(&audioCb)) | |||
| 593 | + | wv.signalIDs = append(wv.signalIDs, sigID) | |||
| 594 | + | } | |||
| 595 | + | ||||
| 570 | // ID returns the unique identifier for this WebView. | 596 | // ID returns the unique identifier for this WebView. | ||
| 571 | func (wv *WebView) ID() WebViewID { | 597 | func (wv *WebView) ID() WebViewID { | ||
| 572 | return wv.id | 598 | return wv.id | ||
| @@ -582,6 +608,16 @@ func (wv *WebView) Widget() *webkit.WebView { | |||||
| 582 | return wv.inner | 608 | return wv.inner | ||
| 583 | } | 609 | } | ||
| 584 | 610 | ||||
| 611 | + | // IsFullscreen returns true if the WebView is currently in fullscreen mode. | |||
| 612 | + | func (wv *WebView) IsFullscreen() bool { | |||
| 613 | + | return wv.isFullscreen.Load() | |||
| 614 | + | } | |||
| 615 | + | ||||
| 616 | + | // IsPlayingAudio returns true if the WebView is currently playing audio. | |||
| 617 | + | func (wv *WebView) IsPlayingAudio() bool { | |||
| 618 | + | return wv.isPlayingAudio.Load() | |||
| 619 | + | } | |||
| 620 | + | ||||
| 585 | // GetFindController returns the WebKit FindController wrapped in the port interface. | 621 | // GetFindController returns the WebKit FindController wrapped in the port interface. | ||
| 586 | // The adapter is cached to prevent the Go wrapper from being garbage collected. | 622 | // The adapter is cached to prevent the Go wrapper from being garbage collected. | ||
| 587 | func (wv *WebView) GetFindController() port.FindController { | 623 | func (wv *WebView) GetFindController() port.FindController { | ||
| @@ -898,6 +934,7 @@ func (wv *WebView) Destroy() { | |||||
| 898 | wv.OnLinkMiddleClick = nil | 934 | wv.OnLinkMiddleClick = nil | ||
| 899 | wv.OnEnterFullscreen = nil | 935 | wv.OnEnterFullscreen = nil | ||
| 900 | wv.OnLeaveFullscreen = nil | 936 | wv.OnLeaveFullscreen = nil | ||
| 937 | + | wv.OnAudioStateChanged = nil | |||
| 901 | 938 | ||||
| 902 | // 3. Clear async callback references | 939 | // 3. Clear async callback references | ||
| 903 | wv.mu.Lock() | 940 | wv.mu.Lock() | ||
M internal/ui/coordinator/content.go +43 -4
| @@ -175,7 +175,7 @@ func (c *ContentCoordinator) EnsureWebView(ctx context.Context, paneID entity.Pa | |||
| 175 | 175 | } | |
| 176 | 176 | ||
| 177 | 177 | // Set up fullscreen handlers for idle inhibition | |
| 178 | - | c.setupFullscreenHandlers(ctx, paneID, wv) | |
| 178 | + | c.setupIdleInhibitionHandlers(ctx, paneID, wv) | |
| 179 | 179 | ||
| 180 | 180 | // Set up popup handling for this WebView | |
| 181 | 181 | c.SetupPopupHandling(ctx, paneID, wv) | |
| @@ -194,6 +194,24 @@ func (c *ContentCoordinator) ReleaseWebView(ctx context.Context, paneID entity.P | |||
| 194 | 194 | } | |
| 195 | 195 | delete(c.webViews, paneID) | |
| 196 | 196 | ||
| 197 | + | // CRITICAL: If this webview was inhibiting idle (fullscreen or audio playing), | |
| 198 | + | // we must release the inhibition before destroying the webview. | |
| 199 | + | // Otherwise the D-Bus inhibit request stays active forever. | |
| 200 | + | if c.idleInhibitor != nil { | |
| 201 | + | if wv.IsFullscreen() { | |
| 202 | + | log.Debug().Str("pane_id", string(paneID)).Msg("releasing idle inhibition (was fullscreen)") | |
| 203 | + | if err := c.idleInhibitor.Uninhibit(ctx); err != nil { | |
| 204 | + | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle on release (fullscreen)") | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | if wv.IsPlayingAudio() { | |
| 208 | + | log.Debug().Str("pane_id", string(paneID)).Msg("releasing idle inhibition (was playing audio)") | |
| 209 | + | if err := c.idleInhibitor.Uninhibit(ctx); err != nil { | |
| 210 | + | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle on release (audio)") | |
| 211 | + | } | |
| 212 | + | } | |
| 213 | + | } | |
| 214 | + | ||
| 197 | 215 | // Clean up title tracking | |
| 198 | 216 | c.titleMu.Lock() | |
| 199 | 217 | delete(c.paneTitles, paneID) | |
| @@ -993,7 +1011,7 @@ func (c *ContentCoordinator) setupWebViewCallbacks(ctx context.Context, paneID e | |||
| 993 | 1011 | } | |
| 994 | 1012 | ||
| 995 | 1013 | // Fullscreen handlers for idle inhibition | |
| 996 | - | c.setupFullscreenHandlers(ctx, paneID, wv) | |
| 1014 | + | c.setupIdleInhibitionHandlers(ctx, paneID, wv) | |
| 997 | 1015 | } | |
| 998 | 1016 | ||
| 999 | 1017 | // setupOAuthAutoClose monitors the popup for OAuth callback URLs and auto-closes. | |
| @@ -1145,14 +1163,19 @@ func (c *ContentCoordinator) handleLinkMiddleClick(ctx context.Context, parentPa | |||
| 1145 | 1163 | return true | |
| 1146 | 1164 | } | |
| 1147 | 1165 | ||
| 1148 | - | // setupFullscreenHandlers configures fullscreen callbacks for idle inhibition. | |
| 1149 | - | func (c *ContentCoordinator) setupFullscreenHandlers(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | |
| 1166 | + | // setupIdleInhibitionHandlers configures fullscreen and audio callbacks for idle inhibition. | |
| 1167 | + | // Idle is inhibited when: | |
| 1168 | + | // - The webview enters fullscreen mode (e.g., fullscreen video) | |
| 1169 | + | // - The webview is playing audio (e.g., video/music playback) | |
| 1170 | + | // The inhibitor uses refcounting, so both can be active simultaneously. | |
| 1171 | + | func (c *ContentCoordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | |
| 1150 | 1172 | log := logging.FromContext(ctx) | |
| 1151 | 1173 | ||
| 1152 | 1174 | if wv == nil { | |
| 1153 | 1175 | return | |
| 1154 | 1176 | } | |
| 1155 | 1177 | ||
| 1178 | + | // Fullscreen handling | |
| 1156 | 1179 | wv.OnEnterFullscreen = func() bool { | |
| 1157 | 1180 | if c.idleInhibitor != nil { | |
| 1158 | 1181 | if err := c.idleInhibitor.Inhibit(ctx, "Fullscreen video playback"); err != nil { | |
| @@ -1170,4 +1193,20 @@ func (c *ContentCoordinator) setupFullscreenHandlers(ctx context.Context, paneID | |||
| 1170 | 1193 | } | |
| 1171 | 1194 | return false // Allow leaving fullscreen | |
| 1172 | 1195 | } | |
| 1196 | + | ||
| 1197 | + | // Audio playback handling | |
| 1198 | + | wv.OnAudioStateChanged = func(playing bool) { | |
| 1199 | + | if c.idleInhibitor == nil { | |
| 1200 | + | return | |
| 1201 | + | } | |
| 1202 | + | if playing { | |
| 1203 | + | if err := c.idleInhibitor.Inhibit(ctx, "Media playback"); err != nil { | |
| 1204 | + | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to inhibit idle for audio") | |
| 1205 | + | } | |
| 1206 | + | } else { | |
| 1207 | + | if err := c.idleInhibitor.Uninhibit(ctx); err != nil { | |
| 1208 | + | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle for audio") | |
| 1209 | + | } | |
| 1210 | + | } | |
| 1211 | + | } | |
| 1173 | 1212 | } | |
| @@ -175,7 +175,7 @@ func (c *ContentCoordinator) EnsureWebView(ctx context.Context, paneID entity.Pa | |||||
| 175 | } | 175 | } | ||
| 176 | 176 | ||||
| 177 | // Set up fullscreen handlers for idle inhibition | 177 | // Set up fullscreen handlers for idle inhibition | ||
| 178 | - | c.setupFullscreenHandlers(ctx, paneID, wv) | 178 | + | c.setupIdleInhibitionHandlers(ctx, paneID, wv) |
| 179 | 179 | ||||
| 180 | // Set up popup handling for this WebView | 180 | // Set up popup handling for this WebView | ||
| 181 | c.SetupPopupHandling(ctx, paneID, wv) | 181 | c.SetupPopupHandling(ctx, paneID, wv) | ||
| @@ -194,6 +194,24 @@ func (c *ContentCoordinator) ReleaseWebView(ctx context.Context, paneID entity.P | |||||
| 194 | } | 194 | } | ||
| 195 | delete(c.webViews, paneID) | 195 | delete(c.webViews, paneID) | ||
| 196 | 196 | ||||
| 197 | + | // CRITICAL: If this webview was inhibiting idle (fullscreen or audio playing), | |||
| 198 | + | // we must release the inhibition before destroying the webview. | |||
| 199 | + | // Otherwise the D-Bus inhibit request stays active forever. | |||
| 200 | + | if c.idleInhibitor != nil { | |||
| 201 | + | if wv.IsFullscreen() { | |||
| 202 | + | log.Debug().Str("pane_id", string(paneID)).Msg("releasing idle inhibition (was fullscreen)") | |||
| 203 | + | if err := c.idleInhibitor.Uninhibit(ctx); err != nil { | |||
| 204 | + | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle on release (fullscreen)") | |||
| 205 | + | } | |||
| 206 | + | } | |||
| 207 | + | if wv.IsPlayingAudio() { | |||
| 208 | + | log.Debug().Str("pane_id", string(paneID)).Msg("releasing idle inhibition (was playing audio)") | |||
| 209 | + | if err := c.idleInhibitor.Uninhibit(ctx); err != nil { | |||
| 210 | + | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle on release (audio)") | |||
| 211 | + | } | |||
| 212 | + | } | |||
| 213 | + | } | |||
| 214 | + | ||||
| 197 | // Clean up title tracking | 215 | // Clean up title tracking | ||
| 198 | c.titleMu.Lock() | 216 | c.titleMu.Lock() | ||
| 199 | delete(c.paneTitles, paneID) | 217 | delete(c.paneTitles, paneID) | ||
| @@ -993,7 +1011,7 @@ func (c *ContentCoordinator) setupWebViewCallbacks(ctx context.Context, paneID e | |||||
| 993 | } | 1011 | } | ||
| 994 | 1012 | ||||
| 995 | // Fullscreen handlers for idle inhibition | 1013 | // Fullscreen handlers for idle inhibition | ||
| 996 | - | c.setupFullscreenHandlers(ctx, paneID, wv) | 1014 | + | c.setupIdleInhibitionHandlers(ctx, paneID, wv) |
| 997 | } | 1015 | } | ||
| 998 | 1016 | ||||
| 999 | // setupOAuthAutoClose monitors the popup for OAuth callback URLs and auto-closes. | 1017 | // setupOAuthAutoClose monitors the popup for OAuth callback URLs and auto-closes. | ||
| @@ -1145,14 +1163,19 @@ func (c *ContentCoordinator) handleLinkMiddleClick(ctx context.Context, parentPa | |||||
| 1145 | return true | 1163 | return true | ||
| 1146 | } | 1164 | } | ||
| 1147 | 1165 | ||||
| 1148 | - | // setupFullscreenHandlers configures fullscreen callbacks for idle inhibition. | 1166 | + | // setupIdleInhibitionHandlers configures fullscreen and audio callbacks for idle inhibition. |
| 1149 | - | func (c *ContentCoordinator) setupFullscreenHandlers(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | 1167 | + | // Idle is inhibited when: |
| 1168 | + | // - The webview enters fullscreen mode (e.g., fullscreen video) | |||
| 1169 | + | // - The webview is playing audio (e.g., video/music playback) | |||
| 1170 | + | // The inhibitor uses refcounting, so both can be active simultaneously. | |||
| 1171 | + | func (c *ContentCoordinator) setupIdleInhibitionHandlers(ctx context.Context, paneID entity.PaneID, wv *webkit.WebView) { | |||
| 1150 | log := logging.FromContext(ctx) | 1172 | log := logging.FromContext(ctx) | ||
| 1151 | 1173 | ||||
| 1152 | if wv == nil { | 1174 | if wv == nil { | ||
| 1153 | return | 1175 | return | ||
| 1154 | } | 1176 | } | ||
| 1155 | 1177 | ||||
| 1178 | + | // Fullscreen handling | |||
| 1156 | wv.OnEnterFullscreen = func() bool { | 1179 | wv.OnEnterFullscreen = func() bool { | ||
| 1157 | if c.idleInhibitor != nil { | 1180 | if c.idleInhibitor != nil { | ||
| 1158 | if err := c.idleInhibitor.Inhibit(ctx, "Fullscreen video playback"); err != nil { | 1181 | if err := c.idleInhibitor.Inhibit(ctx, "Fullscreen video playback"); err != nil { | ||
| @@ -1170,4 +1193,20 @@ func (c *ContentCoordinator) setupFullscreenHandlers(ctx context.Context, paneID | |||||
| 1170 | } | 1193 | } | ||
| 1171 | return false // Allow leaving fullscreen | 1194 | return false // Allow leaving fullscreen | ||
| 1172 | } | 1195 | } | ||
| 1196 | + | ||||
| 1197 | + | // Audio playback handling | |||
| 1198 | + | wv.OnAudioStateChanged = func(playing bool) { | |||
| 1199 | + | if c.idleInhibitor == nil { | |||
| 1200 | + | return | |||
| 1201 | + | } | |||
| 1202 | + | if playing { | |||
| 1203 | + | if err := c.idleInhibitor.Inhibit(ctx, "Media playback"); err != nil { | |||
| 1204 | + | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to inhibit idle for audio") | |||
| 1205 | + | } | |||
| 1206 | + | } else { | |||
| 1207 | + | if err := c.idleInhibitor.Uninhibit(ctx); err != nil { | |||
| 1208 | + | log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle for audio") | |||
| 1209 | + | } | |||
| 1210 | + | } | |||
| 1211 | + | } | |||
| 1173 | } | 1212 | } | ||