diff --git a/server/internal/desktop/clipboard.go b/server/internal/desktop/clipboard.go index fff20d8bc..11e155a3a 100644 --- a/server/internal/desktop/clipboard.go +++ b/server/internal/desktop/clipboard.go @@ -1,3 +1,5 @@ +//go:build !windows + package desktop import ( diff --git a/server/internal/desktop/drop.go b/server/internal/desktop/drop.go index ed102f984..a41b796b8 100644 --- a/server/internal/desktop/drop.go +++ b/server/internal/desktop/drop.go @@ -1,3 +1,5 @@ +//go:build !windows + package desktop import ( diff --git a/server/internal/desktop/filechooserdialog.go b/server/internal/desktop/filechooserdialog.go index 14c702173..f0b5afcf1 100644 --- a/server/internal/desktop/filechooserdialog.go +++ b/server/internal/desktop/filechooserdialog.go @@ -1,3 +1,5 @@ +//go:build !windows + package desktop import ( diff --git a/server/internal/desktop/manager.go b/server/internal/desktop/manager.go index ccae14704..8cdfd2133 100644 --- a/server/internal/desktop/manager.go +++ b/server/internal/desktop/manager.go @@ -1,3 +1,5 @@ +//go:build !windows + package desktop import ( diff --git a/server/internal/desktop/windows.go b/server/internal/desktop/windows.go new file mode 100644 index 000000000..c63be0390 --- /dev/null +++ b/server/internal/desktop/windows.go @@ -0,0 +1,510 @@ +package desktop + +import ( + "bytes" + "errors" + "fmt" + "image" + "os/exec" + "strings" + "sync" + "syscall" + "time" + "unicode/utf16" + "unsafe" + + "github.com/kataras/go-events" + "github.com/m1k1o/neko/server/internal/config" + "github.com/m1k1o/neko/server/pkg/types" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +const ( + inputMouse = 0 + inputKeyboard = 1 + + mouseLeftDown = 0x0002 + mouseLeftUp = 0x0004 + mouseRightDown = 0x0008 + mouseRightUp = 0x0010 + mouseMiddleDown = 0x0020 + mouseMiddleUp = 0x0040 + mouseWheel = 0x0800 + mouseHWheel = 0x1000 + + keyUp = 0x0002 + keyUnicode = 0x0004 + + clipboardTextPlainTarget = "UTF8_STRING" + clipboardTextHTMLTarget = "text/html" +) + +var ( + user32 = syscall.NewLazyDLL("user32.dll") + procSetCursorPos = user32.NewProc("SetCursorPos") + procGetCursorPos = user32.NewProc("GetCursorPos") + procGetSystemMetrics = user32.NewProc("GetSystemMetrics") + procSendInput = user32.NewProc("SendInput") + procVkKeyScanW = user32.NewProc("VkKeyScanW") +) + +type point struct { + X int32 + Y int32 +} + +type input struct { + Type uint32 + _ uint32 + Data [32]byte +} + +type mouseInput struct { + Dx int32 + Dy int32 + MouseData uint32 + Flags uint32 + Time uint32 + ExtraInfo uintptr +} + +type keyboardInput struct { + VirtualKey uint16 + ScanCode uint16 + Flags uint32 + Time uint32 + ExtraInfo uintptr +} + +type keyStroke struct { + virtualKey uint16 + scanCode uint16 + unicode bool +} + +type DesktopManagerCtx struct { + logger zerolog.Logger + emmiter events.EventEmmiter + config *config.Desktop + screenSize types.ScreenSize + + mu sync.Mutex + pressedKeys map[uint32][]keyStroke + pressedButtons map[uint32]bool +} + +func New(config *config.Desktop) *DesktopManagerCtx { + return &DesktopManagerCtx{ + logger: log.With().Str("module", "desktop").Logger(), + emmiter: events.New(), + config: config, + screenSize: config.ScreenSize, + pressedKeys: make(map[uint32][]keyStroke), + pressedButtons: make(map[uint32]bool), + } +} + +func (manager *DesktopManagerCtx) Start() { + width, _, _ := procGetSystemMetrics.Call(0) + height, _, _ := procGetSystemMetrics.Call(1) + manager.screenSize = types.ScreenSize{Width: int(width), Height: int(height), Rate: 25} + manager.logger.Info().Str("screen_size", manager.screenSize.String()).Msg("using Windows desktop") +} + +func (manager *DesktopManagerCtx) Shutdown() error { + manager.ResetKeys() + return nil +} + +func (manager *DesktopManagerCtx) OnBeforeScreenSizeChange(listener func()) { + manager.emmiter.On("before_screen_size_change", func(...any) { listener() }) +} + +func (manager *DesktopManagerCtx) OnAfterScreenSizeChange(listener func()) { + manager.emmiter.On("after_screen_size_change", func(...any) { listener() }) +} + +func (manager *DesktopManagerCtx) Move(x, y int) { + procSetCursorPos.Call(uintptr(x), uintptr(y)) +} + +func (manager *DesktopManagerCtx) GetCursorPosition() (int, int) { + var p point + procGetCursorPos.Call(uintptr(unsafe.Pointer(&p))) + return int(p.X), int(p.Y) +} + +func (manager *DesktopManagerCtx) Scroll(deltaX, deltaY int, controlKey bool) { + if controlKey { + _ = sendKeyboard(keyStroke{virtualKey: 0x11}, false) + defer sendKeyboard(keyStroke{virtualKey: 0x11}, true) + } + if deltaY != 0 { + _ = sendMouse(mouseWheel, uint32(int32(deltaY))) + } + if deltaX != 0 { + _ = sendMouse(mouseHWheel, uint32(int32(deltaX))) + } +} + +func (manager *DesktopManagerCtx) ButtonDown(code uint32) error { + manager.mu.Lock() + defer manager.mu.Unlock() + if manager.pressedButtons[code] { + return nil + } + down, _, ok := mouseFlags(code) + if !ok { + return fmt.Errorf("unknown mouse button %d", code) + } + if err := sendMouse(down, 0); err != nil { + return err + } + manager.pressedButtons[code] = true + return nil +} + +func (manager *DesktopManagerCtx) ButtonUp(code uint32) error { + manager.mu.Lock() + defer manager.mu.Unlock() + _, up, ok := mouseFlags(code) + if !ok { + return fmt.Errorf("unknown mouse button %d", code) + } + if err := sendMouse(up, 0); err != nil { + return err + } + delete(manager.pressedButtons, code) + return nil +} + +func (manager *DesktopManagerCtx) ButtonPress(code uint32) error { + if err := manager.ButtonDown(code); err != nil { + return err + } + return manager.ButtonUp(code) +} + +func (manager *DesktopManagerCtx) KeyDown(code uint32) error { + manager.mu.Lock() + defer manager.mu.Unlock() + if _, ok := manager.pressedKeys[code]; ok { + return nil + } + strokes, err := keysymStrokes(code) + if err != nil { + return err + } + for _, stroke := range strokes { + if err := sendKeyboard(stroke, false); err != nil { + return err + } + } + manager.pressedKeys[code] = strokes + return nil +} + +func (manager *DesktopManagerCtx) KeyUp(code uint32) error { + manager.mu.Lock() + defer manager.mu.Unlock() + strokes, ok := manager.pressedKeys[code] + if !ok { + var err error + strokes, err = keysymStrokes(code) + if err != nil { + return err + } + } + for i := len(strokes) - 1; i >= 0; i-- { + if err := sendKeyboard(strokes[i], true); err != nil { + return err + } + } + delete(manager.pressedKeys, code) + return nil +} + +func (manager *DesktopManagerCtx) KeyPress(codes ...uint32) error { + for _, code := range codes { + if err := manager.KeyDown(code); err != nil { + return err + } + } + if len(codes) > 1 { + time.Sleep(10 * time.Millisecond) + } + for i := len(codes) - 1; i >= 0; i-- { + if err := manager.KeyUp(codes[i]); err != nil { + return err + } + } + return nil +} + +func (manager *DesktopManagerCtx) ResetKeys() { + manager.mu.Lock() + defer manager.mu.Unlock() + for code, strokes := range manager.pressedKeys { + for i := len(strokes) - 1; i >= 0; i-- { + _ = sendKeyboard(strokes[i], true) + } + delete(manager.pressedKeys, code) + } + for code := range manager.pressedButtons { + _, up, ok := mouseFlags(code) + if ok { + _ = sendMouse(up, 0) + } + delete(manager.pressedButtons, code) + } +} + +func (manager *DesktopManagerCtx) ScreenConfigurations() []types.ScreenSize { + return []types.ScreenSize{manager.screenSize} +} + +func (manager *DesktopManagerCtx) SetScreenSize(types.ScreenSize) (types.ScreenSize, error) { + return manager.screenSize, errors.New("screen resizing is not supported on Windows") +} + +func (manager *DesktopManagerCtx) GetScreenSize() types.ScreenSize { return manager.screenSize } + +func (manager *DesktopManagerCtx) SetKeyboardMap(types.KeyboardMap) error { return nil } + +func (manager *DesktopManagerCtx) GetKeyboardMap() (*types.KeyboardMap, error) { + return &types.KeyboardMap{Layout: "us"}, nil +} + +func (manager *DesktopManagerCtx) SetKeyboardModifiers(mod types.KeyboardModifiers) { + setModifier := func(code uint32, desired *bool) { + if desired == nil { + return + } + if *desired { + _ = manager.KeyDown(code) + } else { + _ = manager.KeyUp(code) + } + } + setModifier(0xffe1, mod.Shift) + setModifier(0xffe5, mod.CapsLock) + setModifier(0xffe3, mod.Control) + setModifier(0xffe9, mod.Alt) + setModifier(0xff7f, mod.NumLock) + setModifier(0xffeb, mod.Super) +} + +func (manager *DesktopManagerCtx) GetKeyboardModifiers() types.KeyboardModifiers { + manager.mu.Lock() + defer manager.mu.Unlock() + set := func(code uint32) *bool { + _, ok := manager.pressedKeys[code] + return &ok + } + return types.KeyboardModifiers{ + Shift: set(0xffe1), CapsLock: set(0xffe5), Control: set(0xffe3), + Alt: set(0xffe9), NumLock: set(0xff7f), Super: set(0xffeb), + } +} + +func (manager *DesktopManagerCtx) GetCursorImage() *types.CursorImage { + return &types.CursorImage{ + Width: 1, + Height: 1, + Serial: 1, + Image: image.NewRGBA(image.Rect(0, 0, 1, 1)), + } +} +func (manager *DesktopManagerCtx) GetScreenshotImage() *image.RGBA { return nil } + +func (manager *DesktopManagerCtx) OnCursorChanged(listener func(uint64)) { + manager.emmiter.On("cursor-changed", func(payload ...any) { listener(payload[0].(uint64)) }) +} +func (manager *DesktopManagerCtx) OnClipboardUpdated(listener func()) { + manager.emmiter.On("clipboard-updated", func(...any) { listener() }) +} +func (manager *DesktopManagerCtx) OnFileChooserDialogOpened(listener func()) { + manager.emmiter.On("file-chooser-dialog-opened", func(...any) { listener() }) +} +func (manager *DesktopManagerCtx) OnFileChooserDialogClosed(listener func()) { + manager.emmiter.On("file-chooser-dialog-closed", func(...any) { listener() }) +} +func (manager *DesktopManagerCtx) OnEventError(listener func(uint8, string, uint8, uint8)) { + manager.emmiter.On("event-error", func(payload ...any) { + listener(payload[0].(uint8), payload[1].(string), payload[2].(uint8), payload[3].(uint8)) + }) +} + +func (manager *DesktopManagerCtx) HasTouchSupport() bool { return false } +func (manager *DesktopManagerCtx) TouchBegin(uint32, int, int, uint8) error { + return errors.New("touch input is not supported on Windows") +} +func (manager *DesktopManagerCtx) TouchUpdate(uint32, int, int, uint8) error { + return errors.New("touch input is not supported on Windows") +} +func (manager *DesktopManagerCtx) TouchEnd(uint32, int, int, uint8) error { + return errors.New("touch input is not supported on Windows") +} + +func (manager *DesktopManagerCtx) ClipboardGetText() (*types.ClipboardText, error) { + data, err := manager.ClipboardGetBinary(clipboardTextPlainTarget) + if err != nil { + return nil, err + } + return &types.ClipboardText{Text: string(data)}, nil +} + +func (manager *DesktopManagerCtx) ClipboardSetText(data types.ClipboardText) error { + if data.HTML != "" { + return manager.ClipboardSetBinary(clipboardTextHTMLTarget, []byte(data.HTML)) + } + return manager.ClipboardSetBinary(clipboardTextPlainTarget, []byte(data.Text)) +} + +func (manager *DesktopManagerCtx) ClipboardGetBinary(mime string) ([]byte, error) { + if mime != clipboardTextPlainTarget && mime != "text/plain" { + return nil, fmt.Errorf("unsupported clipboard target %q", mime) + } + cmd := hiddenPowerShell("Get-Clipboard -Raw -Format Text") + var stderr bytes.Buffer + cmd.Stderr = &stderr + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("get clipboard: %s", strings.TrimSpace(stderr.String())) + } + return output, nil +} + +func (manager *DesktopManagerCtx) ClipboardSetBinary(mime string, data []byte) error { + if mime != clipboardTextPlainTarget && mime != "text/plain" && mime != clipboardTextHTMLTarget { + return fmt.Errorf("unsupported clipboard target %q", mime) + } + cmd := hiddenPowerShell("Set-Clipboard -Value ([Console]::In.ReadToEnd())") + cmd.Stdin = bytes.NewReader(data) + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("set clipboard: %s", strings.TrimSpace(stderr.String())) + } + manager.emmiter.Emit("clipboard-updated") + return nil +} + +func (manager *DesktopManagerCtx) ClipboardGetTargets() ([]string, error) { + return []string{"text/plain"}, nil +} + +func (manager *DesktopManagerCtx) DropFiles(int, int, []string) bool { return false } +func (manager *DesktopManagerCtx) IsUploadDropEnabled() bool { return false } +func (manager *DesktopManagerCtx) HandleFileChooserDialog(string) error { + return errors.New("file chooser handling is not supported on Windows") +} +func (manager *DesktopManagerCtx) CloseFileChooserDialog() {} +func (manager *DesktopManagerCtx) IsFileChooserDialogEnabled() bool { return false } +func (manager *DesktopManagerCtx) IsFileChooserDialogOpened() bool { return false } + +func hiddenPowerShell(script string) *exec.Cmd { + cmd := exec.Command("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-STA", "-Command", script) + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + return cmd +} + +func mouseFlags(code uint32) (uint32, uint32, bool) { + switch code { + case 1: + return mouseLeftDown, mouseLeftUp, true + case 2: + return mouseMiddleDown, mouseMiddleUp, true + case 3: + return mouseRightDown, mouseRightUp, true + default: + return 0, 0, false + } +} + +func sendMouse(flags, data uint32) error { + in := input{Type: inputMouse} + *(*mouseInput)(unsafe.Pointer(&in.Data[0])) = mouseInput{MouseData: data, Flags: flags} + return sendInput(&in) +} + +func sendKeyboard(stroke keyStroke, up bool) error { + in := input{Type: inputKeyboard} + flags := uint32(0) + if stroke.unicode { + flags |= keyUnicode + } + if up { + flags |= keyUp + } + *(*keyboardInput)(unsafe.Pointer(&in.Data[0])) = keyboardInput{ + VirtualKey: stroke.virtualKey, + ScanCode: stroke.scanCode, + Flags: flags, + } + return sendInput(&in) +} + +func sendInput(in *input) error { + result, _, callErr := procSendInput.Call(1, uintptr(unsafe.Pointer(in)), unsafe.Sizeof(*in)) + if result == 0 { + return fmt.Errorf("SendInput: %w", callErr) + } + return nil +} + +func keysymStrokes(code uint32) ([]keyStroke, error) { + if code&0xff000000 == 0x01000000 { + return unicodeStrokes(rune(code & 0x00ffffff)), nil + } + if code >= 'a' && code <= 'z' { + return []keyStroke{{virtualKey: uint16(code - 'a' + 'A')}}, nil + } + if code >= 'A' && code <= 'Z' || code >= '0' && code <= '9' { + return []keyStroke{{virtualKey: uint16(code)}}, nil + } + if code >= 0x20 && code <= 0xff { + mapped, _, _ := procVkKeyScanW.Call(uintptr(code)) + if uint16(mapped) != 0xffff { + var strokes []keyStroke + modifiers := uint8(mapped >> 8) + if modifiers&1 != 0 { + strokes = append(strokes, keyStroke{virtualKey: 0x10}) + } + if modifiers&2 != 0 { + strokes = append(strokes, keyStroke{virtualKey: 0x11}) + } + if modifiers&4 != 0 { + strokes = append(strokes, keyStroke{virtualKey: 0x12}) + } + return append(strokes, keyStroke{virtualKey: uint16(mapped) & 0xff}), nil + } + return unicodeStrokes(rune(code)), nil + } + if code >= 0xffbe && code <= 0xffc9 { + return []keyStroke{{virtualKey: uint16(0x70 + code - 0xffbe)}}, nil + } + + keys := map[uint32]uint16{ + 0xff08: 0x08, 0xff09: 0x09, 0xff0d: 0x0d, 0xff1b: 0x1b, + 0xff50: 0x24, 0xff51: 0x25, 0xff52: 0x26, 0xff53: 0x27, 0xff54: 0x28, + 0xff55: 0x21, 0xff56: 0x22, 0xff57: 0x23, 0xff63: 0x2d, 0xffff: 0x2e, + 0xffe1: 0x10, 0xffe2: 0x10, 0xffe3: 0x11, 0xffe4: 0x11, + 0xffe9: 0x12, 0xffea: 0x12, 0xffeb: 0x5b, 0xffec: 0x5c, + 0xffe5: 0x14, 0xff7f: 0x90, + } + if key, ok := keys[code]; ok { + return []keyStroke{{virtualKey: key}}, nil + } + return nil, fmt.Errorf("unsupported keysym %#x", code) +} + +func unicodeStrokes(r rune) []keyStroke { + units := utf16.Encode([]rune{r}) + strokes := make([]keyStroke, len(units)) + for i, unit := range units { + strokes[i] = keyStroke{scanCode: unit, unicode: true} + } + return strokes +} diff --git a/server/internal/desktop/xevent.go b/server/internal/desktop/xevent.go index 5118fb416..6101d7584 100644 --- a/server/internal/desktop/xevent.go +++ b/server/internal/desktop/xevent.go @@ -1,3 +1,5 @@ +//go:build !windows + package desktop import ( diff --git a/server/internal/desktop/xinput.go b/server/internal/desktop/xinput.go index bc17461a5..328262a54 100644 --- a/server/internal/desktop/xinput.go +++ b/server/internal/desktop/xinput.go @@ -1,3 +1,5 @@ +//go:build !windows + package desktop import "github.com/m1k1o/neko/server/pkg/xinput" diff --git a/server/internal/desktop/xorg.go b/server/internal/desktop/xorg.go index 907f1abc1..c8e8bf711 100644 --- a/server/internal/desktop/xorg.go +++ b/server/internal/desktop/xorg.go @@ -1,3 +1,5 @@ +//go:build !windows + package desktop import ( diff --git a/server/pkg/gst/gst.c b/server/pkg/gst/gst.c index f1258a8bd..836291f09 100644 --- a/server/pkg/gst/gst.c +++ b/server/pkg/gst/gst.c @@ -1,3 +1,5 @@ +//go:build !windows + #include "gst.h" static void gstreamer_pipeline_log(GstPipelineCtx *ctx, char* level, const char* format, ...) { diff --git a/server/pkg/gst/gst.go b/server/pkg/gst/gst.go index a9aa67092..baa5a3485 100644 --- a/server/pkg/gst/gst.go +++ b/server/pkg/gst/gst.go @@ -1,3 +1,5 @@ +//go:build !windows + package gst /* @@ -31,25 +33,6 @@ func init() { registry = C.gst_registry_get() } -type Pipeline interface { - Src() string - Sample() chan types.Sample - // attach sink or src to pipeline - AttachAppsink(sinkName string) - AttachAppsrc(srcName string) - // control pipeline lifecycle - Play() - Pause() - Destroy() - Push(buffer []byte) - // modify the property of a bin - SetPropInt(binName string, prop string, value int) bool - SetCapsFramerate(binName string, numerator, denominator int) bool - SetCapsResolution(binName string, width, height int) bool - // emit video keyframe - EmitVideoKeyframe() bool -} - type pipeline struct { id int logger zerolog.Logger diff --git a/server/pkg/gst/gst_windows.go b/server/pkg/gst/gst_windows.go new file mode 100644 index 000000000..b34126966 --- /dev/null +++ b/server/pkg/gst/gst_windows.go @@ -0,0 +1,187 @@ +package gst + +import ( + "bufio" + "context" + "encoding/binary" + "fmt" + "io" + "os/exec" + "strings" + "sync" + "syscall" + "time" + + "github.com/m1k1o/neko/server/pkg/types" + "github.com/rs/zerolog/log" +) + +type pipeline struct { + src string + sample chan types.Sample + cancel context.CancelFunc + wg sync.WaitGroup + mu sync.Mutex + started bool +} + +func CreatePipeline(src string) (Pipeline, error) { + if !strings.Contains(src, "windowsdesktop") && !strings.Contains(src, "windowssilence") { + return nil, fmt.Errorf("unsupported Windows capture pipeline %q", src) + } + + return &pipeline{ + src: src, + sample: make(chan types.Sample), + }, nil +} + +func (p *pipeline) Src() string { return p.src } +func (p *pipeline) Sample() chan types.Sample { return p.sample } +func (p *pipeline) AttachAppsink(string) {} +func (p *pipeline) AttachAppsrc(string) {} +func (p *pipeline) Push([]byte) {} +func (p *pipeline) SetPropInt(string, string, int) bool { return false } +func (p *pipeline) SetCapsFramerate(string, int, int) bool { return false } +func (p *pipeline) SetCapsResolution(string, int, int) bool { return false } +func (p *pipeline) EmitVideoKeyframe() bool { return false } + +func (p *pipeline) Play() { + p.mu.Lock() + defer p.mu.Unlock() + if p.started { + return + } + + ctx, cancel := context.WithCancel(context.Background()) + p.cancel = cancel + p.started = true + p.wg.Add(1) + if strings.Contains(p.src, "windowssilence") { + go p.streamSilence(ctx) + } else { + go p.streamDesktop(ctx) + } +} + +func (p *pipeline) Pause() { + p.stop() + p.wg.Wait() +} + +func (p *pipeline) Destroy() { + p.stop() + p.wg.Wait() + close(p.sample) +} + +func (p *pipeline) stop() { + p.mu.Lock() + defer p.mu.Unlock() + if p.cancel != nil { + p.cancel() + p.cancel = nil + } + p.started = false +} + +func (p *pipeline) streamSilence(ctx context.Context) { + defer p.wg.Done() + ticker := time.NewTicker(20 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case timestamp := <-ticker.C: + p.sample <- types.Sample{ + Data: []byte{0xf8, 0xff, 0xfe}, + Length: 3, + Timestamp: timestamp, + Duration: 20 * time.Millisecond, + } + } + } +} + +func (p *pipeline) streamDesktop(ctx context.Context) { + defer p.wg.Done() + + cmd := exec.CommandContext(ctx, "ffmpeg.exe", + "-hide_banner", "-loglevel", "warning", + "-f", "gdigrab", "-framerate", "25", "-draw_mouse", "1", "-i", "desktop", + "-an", "-c:v", "libvpx", "-deadline", "realtime", "-cpu-used", "8", + "-b:v", "2500k", "-g", "25", "-pix_fmt", "yuv420p", "-f", "ivf", "pipe:1", + ) + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + + stdout, err := cmd.StdoutPipe() + if err != nil { + log.Error().Err(err).Msg("create ffmpeg stdout pipe") + return + } + stderr, err := cmd.StderrPipe() + if err != nil { + log.Error().Err(err).Msg("create ffmpeg stderr pipe") + return + } + if err := cmd.Start(); err != nil { + log.Error().Err(err).Msg("start Windows desktop capture") + return + } + + go func() { + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + log.Warn().Str("module", "capture").Msg(scanner.Text()) + } + }() + + if err := p.readIVF(ctx, stdout); err != nil && ctx.Err() == nil { + log.Error().Err(err).Msg("read Windows desktop capture") + } + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + log.Error().Err(err).Msg("Windows desktop capture exited") + } +} + +func (p *pipeline) readIVF(ctx context.Context, r io.Reader) error { + header := make([]byte, 32) + if _, err := io.ReadFull(r, header); err != nil { + return err + } + if string(header[:4]) != "DKIF" { + return fmt.Errorf("unexpected IVF signature %q", header[:4]) + } + + frameHeader := make([]byte, 12) + for { + if _, err := io.ReadFull(r, frameHeader); err != nil { + return err + } + size := binary.LittleEndian.Uint32(frameHeader[:4]) + if size == 0 || size > 16<<20 { + return fmt.Errorf("invalid IVF frame size %d", size) + } + frame := make([]byte, size) + if _, err := io.ReadFull(r, frame); err != nil { + return err + } + + sample := types.Sample{ + Data: frame, + Length: len(frame), + Timestamp: time.Now(), + Duration: 40 * time.Millisecond, + DeltaUnit: frame[0]&1 != 0, + } + select { + case <-ctx.Done(): + return ctx.Err() + case p.sample <- sample: + } + } +} + +func CheckPlugins([]string) error { return nil } diff --git a/server/pkg/gst/pipeline.go b/server/pkg/gst/pipeline.go new file mode 100644 index 000000000..debb641b1 --- /dev/null +++ b/server/pkg/gst/pipeline.go @@ -0,0 +1,18 @@ +package gst + +import "github.com/m1k1o/neko/server/pkg/types" + +type Pipeline interface { + Src() string + Sample() chan types.Sample + AttachAppsink(sinkName string) + AttachAppsrc(srcName string) + Play() + Pause() + Destroy() + Push(buffer []byte) + SetPropInt(binName string, prop string, value int) bool + SetCapsFramerate(binName string, numerator, denominator int) bool + SetCapsResolution(binName string, width, height int) bool + EmitVideoKeyframe() bool +} diff --git a/server/pkg/xorg/xorg.c b/server/pkg/xorg/xorg.c index 3f8aa2136..9474ba80c 100644 --- a/server/pkg/xorg/xorg.c +++ b/server/pkg/xorg/xorg.c @@ -1,3 +1,5 @@ +//go:build !windows + #include "xorg.h" static Display *DISPLAY = NULL; diff --git a/server/pkg/xorg/xorg.go b/server/pkg/xorg/xorg.go index 8d5ee050d..64cb5b894 100644 --- a/server/pkg/xorg/xorg.go +++ b/server/pkg/xorg/xorg.go @@ -1,3 +1,5 @@ +//go:build !windows + package xorg /*