mirror of https://github.com/jetkvm/kvm.git
feat: add HDMI sleep mode (#881)
This commit is contained in:
parent
b144d9926f
commit
cc9ff74276
|
@ -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 {
|
||||
|
|
|
@ -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{},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
|
|
3
main.go
3
main.go
|
@ -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
103
video.go
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
38
webrtc.go
38
webrtc.go
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue