feat: add HDMI sleep mode (#881)

This commit is contained in:
Aveline 2025-10-09 14:52:51 +02:00 committed by GitHub
parent b144d9926f
commit cc9ff74276
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 212 additions and 7 deletions

View File

@ -104,6 +104,7 @@ type Config struct {
UsbDevices *usbgadget.Devices `json:"usb_devices"`
NetworkConfig *network.NetworkConfig `json:"network_config"`
DefaultLogLevel string `json:"default_log_level"`
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
}
func (c *Config) GetDisplayRotation() uint16 {

View File

@ -19,6 +19,7 @@ type Native struct {
onVideoFrameReceived func(frame []byte, duration time.Duration)
onIndevEvent func(event string)
onRpcEvent func(event string)
sleepModeSupported bool
videoLock sync.Mutex
screenLock sync.Mutex
}
@ -62,6 +63,8 @@ func NewNative(opts NativeOptions) *Native {
}
}
sleepModeSupported := isSleepModeSupported()
return &Native{
ready: make(chan struct{}),
l: nativeLogger,
@ -73,6 +76,7 @@ func NewNative(opts NativeOptions) *Native {
onVideoFrameReceived: onVideoFrameReceived,
onIndevEvent: onIndevEvent,
onRpcEvent: onRpcEvent,
sleepModeSupported: sleepModeSupported,
videoLock: sync.Mutex{},
screenLock: sync.Mutex{},
}

View File

@ -1,5 +1,12 @@
package native
import (
"os"
)
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
// VideoState is the state of the video stream.
type VideoState struct {
Ready bool `json:"ready"`
Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range
@ -8,6 +15,58 @@ type VideoState struct {
FramePerSecond float64 `json:"fps"`
}
func isSleepModeSupported() bool {
_, err := os.Stat(sleepModeFile)
return err == nil
}
func (n *Native) setSleepMode(enabled bool) error {
if !n.sleepModeSupported {
return nil
}
bEnabled := "0"
if enabled {
bEnabled = "1"
}
return os.WriteFile(sleepModeFile, []byte(bEnabled), 0644)
}
func (n *Native) getSleepMode() (bool, error) {
if !n.sleepModeSupported {
return false, nil
}
data, err := os.ReadFile(sleepModeFile)
if err == nil {
return string(data) == "1", nil
}
return false, nil
}
// VideoSetSleepMode sets the sleep mode for the video stream.
func (n *Native) VideoSetSleepMode(enabled bool) error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
return n.setSleepMode(enabled)
}
// VideoGetSleepMode gets the sleep mode for the video stream.
func (n *Native) VideoGetSleepMode() (bool, error) {
n.videoLock.Lock()
defer n.videoLock.Unlock()
return n.getSleepMode()
}
// VideoSleepModeSupported checks if the sleep mode is supported.
func (n *Native) VideoSleepModeSupported() bool {
return n.sleepModeSupported
}
// VideoSetQualityFactor sets the quality factor for the video stream.
func (n *Native) VideoSetQualityFactor(factor float64) error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
@ -15,6 +74,7 @@ func (n *Native) VideoSetQualityFactor(factor float64) error {
return videoSetStreamQualityFactor(factor)
}
// VideoGetQualityFactor gets the quality factor for the video stream.
func (n *Native) VideoGetQualityFactor() (float64, error) {
n.videoLock.Lock()
defer n.videoLock.Unlock()
@ -22,6 +82,7 @@ func (n *Native) VideoGetQualityFactor() (float64, error) {
return videoGetStreamQualityFactor()
}
// VideoSetEDID sets the EDID for the video stream.
func (n *Native) VideoSetEDID(edid string) error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
@ -29,6 +90,7 @@ func (n *Native) VideoSetEDID(edid string) error {
return videoSetEDID(edid)
}
// VideoGetEDID gets the EDID for the video stream.
func (n *Native) VideoGetEDID() (string, error) {
n.videoLock.Lock()
defer n.videoLock.Unlock()
@ -36,6 +98,7 @@ func (n *Native) VideoGetEDID() (string, error) {
return videoGetEDID()
}
// VideoLogStatus gets the log status for the video stream.
func (n *Native) VideoLogStatus() (string, error) {
n.videoLock.Lock()
defer n.videoLock.Unlock()
@ -43,6 +106,7 @@ func (n *Native) VideoLogStatus() (string, error) {
return videoLogStatus(), nil
}
// VideoStop stops the video stream.
func (n *Native) VideoStop() error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
@ -51,10 +115,14 @@ func (n *Native) VideoStop() error {
return nil
}
// VideoStart starts the video stream.
func (n *Native) VideoStart() error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
// disable sleep mode before starting video
_ = n.setSleepMode(false)
videoStart()
return nil
}

View File

@ -1215,6 +1215,8 @@ var rpcHandlers = map[string]RPCHandler{
"getEDID": {Func: rpcGetEDID},
"setEDID": {Func: rpcSetEDID, Params: []string{"edid"}},
"getVideoLogStatus": {Func: rpcGetVideoLogStatus},
"getVideoSleepMode": {Func: rpcGetVideoSleepMode},
"setVideoSleepMode": {Func: rpcSetVideoSleepMode, Params: []string{"duration"}},
"getDevChannelState": {Func: rpcGetDevChannelState},
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
"getLocalVersion": {Func: rpcGetLocalVersion},

View File

@ -77,6 +77,9 @@ func Main() {
// initialize display
initDisplay()
// start video sleep mode timer
startVideoSleepModeTicker()
go func() {
time.Sleep(15 * time.Minute)
for {

103
video.go
View File

@ -1,10 +1,22 @@
package kvm
import (
"context"
"fmt"
"time"
"github.com/jetkvm/kvm/internal/native"
)
var lastVideoState native.VideoState
var (
lastVideoState native.VideoState
videoSleepModeCtx context.Context
videoSleepModeCancel context.CancelFunc
)
const (
defaultVideoSleepModeDuration = 1 * time.Minute
)
func triggerVideoStateUpdate() {
go func() {
@ -17,3 +29,92 @@ func triggerVideoStateUpdate() {
func rpcGetVideoState() (native.VideoState, error) {
return lastVideoState, nil
}
type rpcVideoSleepModeResponse struct {
Supported bool `json:"supported"`
Enabled bool `json:"enabled"`
Duration int `json:"duration"`
}
func rpcGetVideoSleepMode() rpcVideoSleepModeResponse {
sleepMode, _ := nativeInstance.VideoGetSleepMode()
return rpcVideoSleepModeResponse{
Supported: nativeInstance.VideoSleepModeSupported(),
Enabled: sleepMode,
Duration: config.VideoSleepAfterSec,
}
}
func rpcSetVideoSleepMode(duration int) error {
if duration < 0 {
duration = -1 // disable
}
config.VideoSleepAfterSec = duration
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
// we won't restart the ticker here,
// as the session can't be inactive when this function is called
return nil
}
func stopVideoSleepModeTicker() {
nativeLogger.Trace().Msg("stopping HDMI sleep mode ticker")
if videoSleepModeCancel != nil {
nativeLogger.Trace().Msg("canceling HDMI sleep mode ticker context")
videoSleepModeCancel()
videoSleepModeCancel = nil
videoSleepModeCtx = nil
}
}
func startVideoSleepModeTicker() {
if !nativeInstance.VideoSleepModeSupported() {
return
}
var duration time.Duration
if config.VideoSleepAfterSec == 0 {
duration = defaultVideoSleepModeDuration
} else if config.VideoSleepAfterSec > 0 {
duration = time.Duration(config.VideoSleepAfterSec) * time.Second
} else {
stopVideoSleepModeTicker()
return
}
// Stop any existing timer and goroutine
stopVideoSleepModeTicker()
// Create new context for this ticker
videoSleepModeCtx, videoSleepModeCancel = context.WithCancel(context.Background())
go doVideoSleepModeTicker(videoSleepModeCtx, duration)
}
func doVideoSleepModeTicker(ctx context.Context, duration time.Duration) {
timer := time.NewTimer(duration)
defer timer.Stop()
nativeLogger.Trace().Msg("HDMI sleep mode ticker started")
for {
select {
case <-timer.C:
if getActiveSessions() > 0 {
nativeLogger.Warn().Msg("not going to enter HDMI sleep mode because there are active sessions")
continue
}
nativeLogger.Trace().Msg("entering HDMI sleep mode")
_ = nativeInstance.VideoSetSleepMode(true)
case <-ctx.Done():
nativeLogger.Trace().Msg("HDMI sleep mode ticker stopped")
return
}
}
}

View File

@ -39,6 +39,34 @@ type Session struct {
keysDownStateQueue chan usbgadget.KeysDownState
}
var (
actionSessions int = 0
activeSessionsMutex = &sync.Mutex{}
)
func incrActiveSessions() int {
activeSessionsMutex.Lock()
defer activeSessionsMutex.Unlock()
actionSessions++
return actionSessions
}
func decrActiveSessions() int {
activeSessionsMutex.Lock()
defer activeSessionsMutex.Unlock()
actionSessions--
return actionSessions
}
func getActiveSessions() int {
activeSessionsMutex.Lock()
defer activeSessionsMutex.Unlock()
return actionSessions
}
func (s *Session) resetKeepAliveTime() {
s.keepAliveJitterLock.Lock()
defer s.keepAliveJitterLock.Unlock()
@ -312,9 +340,8 @@ func newSession(config SessionConfig) (*Session, error) {
if connectionState == webrtc.ICEConnectionStateConnected {
if !isConnected {
isConnected = true
actionSessions++
onActiveSessionsChanged()
if actionSessions == 1 {
if incrActiveSessions() == 1 {
onFirstSessionConnected()
}
}
@ -353,9 +380,8 @@ func newSession(config SessionConfig) (*Session, error) {
}
if isConnected {
isConnected = false
actionSessions--
onActiveSessionsChanged()
if actionSessions == 0 {
if decrActiveSessions() == 0 {
onLastSessionDisconnected()
}
}
@ -364,16 +390,16 @@ func newSession(config SessionConfig) (*Session, error) {
return session, nil
}
var actionSessions = 0
func onActiveSessionsChanged() {
requestDisplayUpdate(true, "active_sessions_changed")
}
func onFirstSessionConnected() {
_ = nativeInstance.VideoStart()
stopVideoSleepModeTicker()
}
func onLastSessionDisconnected() {
_ = nativeInstance.VideoStop()
startVideoSleepModeTicker()
}