From d5a0b1a8b36dc74f2b083e1d390957aa5d302404 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Wed, 1 Jan 2025 14:26:22 +0000 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=95=B4=E5=90=88=EF=BC=9A?= =?UTF-8?q?=201.=20python=20=E5=86=85=E7=BD=AE=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=202.=20=E5=B0=86=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 19 - .editorconfig | 10 - .gitignore | 3 + Makefile | 23 +- build/Dockerfile | 20 +- configs/nginx/kvmd.ctx-http.conf | 7 - configs/os/boot-config/v0-hdmi-rpi2.txt | 7 - configs/os/boot-config/v0-hdmi-rpi3.txt | 8 - configs/os/boot-config/v0-hdmi-zero2w.txt | 8 - configs/os/boot-config/v0-hdmiusb-rpi2.txt | 6 - configs/os/boot-config/v0-hdmiusb-rpi3.txt | 7 - configs/os/boot-config/v0-hdmiusb-zero2w.txt | 7 - configs/os/boot-config/v1-hdmi-rpi2.txt | 8 - configs/os/boot-config/v1-hdmi-rpi3.txt | 9 - configs/os/boot-config/v1-hdmi-zero2w.txt | 9 - configs/os/boot-config/v1-hdmiusb-rpi2.txt | 7 - configs/os/boot-config/v1-hdmiusb-rpi3.txt | 8 - configs/os/boot-config/v1-hdmiusb-zero2w.txt | 8 - configs/os/boot-config/v2-hdmi-rpi3.txt | 9 - configs/os/boot-config/v2-hdmi-rpi4.txt | 9 - configs/os/boot-config/v2-hdmi-zero2w.txt | 9 - configs/os/boot-config/v2-hdmiusb-rpi4.txt | 8 - configs/os/boot-config/v3-hdmi-rpi4.txt | 22 - configs/os/boot-config/v4mini-hdmi-rpi4.txt | 21 - configs/os/boot-config/v4plus-hdmi-rpi4.txt | 29 - configs/os/cmdline/v0-hdmi-rpi2.sed | 5 - configs/os/cmdline/v0-hdmi-rpi3.sed | 5 - configs/os/cmdline/v0-hdmi-zero2w.sed | 5 - configs/os/cmdline/v0-hdmiusb-rpi2.sed | 4 - configs/os/cmdline/v0-hdmiusb-rpi3.sed | 4 - configs/os/cmdline/v0-hdmiusb-zero2w.sed | 4 - configs/os/cmdline/v1-hdmi-rpi2.sed | 1 - configs/os/cmdline/v1-hdmi-rpi3.sed | 1 - configs/os/cmdline/v1-hdmi-zero2w.sed | 1 - configs/os/cmdline/v2-hdmi-rpi3.sed | 1 - configs/os/cmdline/v2-hdmi-rpi4.sed | 1 - configs/os/cmdline/v2-hdmi-zero2w.sed | 1 - configs/os/cmdline/v2-hdmiusb-rpi4.sed | 0 configs/os/cmdline/v3-hdmi-rpi4.sed | 1 - configs/os/cmdline/v4mini-hdmi-rpi4.sed | 1 - configs/os/cmdline/v4plus-hdmi-rpi4.sed | 1 - configs/os/kvmd-webterm.conf | 6 - configs/os/modules-load/v0-hdmi.conf | 1 - configs/os/modules-load/v1-hdmi.conf | 1 - configs/os/modules-load/v2-hdmi.conf | 3 - configs/os/modules-load/v2-hdmiusb.conf | 2 - configs/os/modules-load/v3-hdmi.conf | 4 - configs/os/modules-load/v4mini-hdmi.conf | 4 - configs/os/modules-load/v4plus-hdmi.conf | 4 - configs/os/services/kvmd-bootconfig.service | 29 - configs/os/services/kvmd-certbot.service | 9 - configs/os/services/kvmd-certbot.timer | 10 - configs/os/services/kvmd-ipmi.service | 17 - configs/os/services/kvmd-janus-static.service | 22 - configs/os/services/kvmd-janus.service | 22 - configs/os/services/kvmd-nginx.service | 21 - configs/os/services/kvmd-oled-reboot.service | 12 - .../os/services/kvmd-oled-shutdown.service | 14 - configs/os/services/kvmd-oled.service | 15 - configs/os/services/kvmd-otg.service | 13 - configs/os/services/kvmd-otgnet.service | 14 - configs/os/services/kvmd-pst.service | 16 - configs/os/services/kvmd-tc358743.service | 14 - configs/os/services/kvmd-vnc.service | 16 - configs/os/services/kvmd-watchdog.service | 14 - configs/os/services/kvmd-webterm.service | 24 - configs/os/services/kvmd.service | 18 - configs/os/sudoers/v0-hdmi | 1 - configs/os/sudoers/v0-hdmiusb | 1 - configs/os/sudoers/v1-hdmi | 1 - configs/os/sudoers/v1-hdmiusb | 1 - configs/os/sudoers/v2-hdmi | 2 - configs/os/sudoers/v2-hdmiusb | 2 - configs/os/sudoers/v3-hdmi | 2 - configs/os/sudoers/v4mini-hdmi | 2 - configs/os/sudoers/v4plus-hdmi | 2 - configs/os/sysctl.conf | 10 - configs/os/sysusers.conf | 38 - configs/os/tmpfiles.conf | 7 - configs/os/udev/common.rules | 3 - configs/os/udev/v0-hdmi-rpi2.rules | 4 - configs/os/udev/v0-hdmi-rpi3.rules | 4 - configs/os/udev/v0-hdmi-zero2w.rules | 4 - configs/os/udev/v0-hdmiusb-rpi2.rules | 4 - configs/os/udev/v0-hdmiusb-rpi3.rules | 4 - configs/os/udev/v0-hdmiusb-zero2w.rules | 4 - configs/os/udev/v1-hdmi-rpi2.rules | 3 - configs/os/udev/v1-hdmi-rpi3.rules | 3 - configs/os/udev/v1-hdmi-zero2w.rules | 3 - configs/os/udev/v1-hdmiusb-rpi2.rules | 3 - configs/os/udev/v1-hdmiusb-rpi3.rules | 3 - configs/os/udev/v1-hdmiusb-zero2w.rules | 3 - configs/os/udev/v2-hdmi-rpi3.rules | 6 - configs/os/udev/v2-hdmi-rpi4.rules | 6 - configs/os/udev/v2-hdmi-zero2w.rules | 6 - configs/os/udev/v2-hdmiusb-generic.rules | 7 - configs/os/udev/v2-hdmiusb-rpi4.rules | 6 - configs/os/udev/v3-hdmi-rpi4.rules | 6 - configs/os/udev/v4mini-hdmi-rpi4.rules | 6 - configs/os/udev/v4plus-hdmi-rpi4.rules | 7 - contrib/README | 3 - hid/arduino/.gitignore | 6 - hid/arduino/Makefile | 51 - hid/arduino/avrdude-rpi.conf | 7 - hid/arduino/avrdude.py | 54 - hid/arduino/lib/drivers-avr/eeprom.h | 40 - hid/arduino/lib/drivers-avr/factory.cpp | 94 - hid/arduino/lib/drivers-avr/ps2/hid.h | 95 - hid/arduino/lib/drivers-avr/ps2/keymap.h | 152 - hid/arduino/lib/drivers-avr/ps2/keymap.h.mako | 44 - hid/arduino/lib/drivers-avr/spi.cpp | 83 - hid/arduino/lib/drivers-avr/spi.h | 40 - hid/arduino/lib/drivers-avr/usb/hid.h | 230 -- hid/arduino/lib/drivers-stm32/README.md | 22 - .../lib/drivers-stm32/backup-register.h | 53 - .../lib/drivers-stm32/bluepill_sch.png | Bin 179860 -> 0 bytes hid/arduino/lib/drivers-stm32/board-stm32.h | 103 - hid/arduino/lib/drivers-stm32/factory.cpp | 94 - .../lib/drivers-stm32/usb/hid-wrapper-stm32.h | 72 - .../lib/drivers-stm32/usb/keyboard-stm32.h | 92 - .../drivers-stm32/usb/mouse-absolute-stm32.h | 86 - .../drivers-stm32/usb/mouse-relative-stm32.h | 86 - hid/arduino/lib/drivers/aum.h | 48 - hid/arduino/lib/drivers/board.h | 41 - hid/arduino/lib/drivers/connection.h | 54 - hid/arduino/lib/drivers/driver.h | 49 - hid/arduino/lib/drivers/factory.h | 39 - hid/arduino/lib/drivers/keyboard.h | 68 - hid/arduino/lib/drivers/mouse.h | 51 - hid/arduino/lib/drivers/serial.h | 68 - hid/arduino/lib/drivers/storage.h | 35 - hid/arduino/lib/drivers/tools.cpp | 32 - hid/arduino/lib/drivers/tools.h | 28 - hid/arduino/lib/drivers/usb-keymap.h | 141 - hid/arduino/lib/drivers/usb-keymap.h.mako | 37 - hid/arduino/patch.py | 49 - .../arduino-get-plugged-endpoint.patch | 11 - hid/arduino/patches/arduino-main-no-usb.patch | 24 - .../patches/arduino-optional-cdc.patch | 141 - hid/arduino/patches/hid-no-singletones.patch | 66 - hid/arduino/patches/hid-shut-up.patch | 16 - hid/arduino/patches/hid-win98.patch | 82 - .../platformio-stm32f1-no-serial-usb.patch | 11 - hid/arduino/platformio-avr.ini | 119 - hid/arduino/platformio-stm32.ini | 52 - hid/arduino/src/main.cpp | 253 -- hid/arduino/src/outputs.h | 122 - hid/arduino/src/proto.h | 142 - hid/pico/.gitignore | 5 - hid/pico/CMakeLists.txt | 22 - hid/pico/Makefile | 38 - hid/pico/src/CMakeLists.txt | 39 - hid/pico/src/main.c | 130 - hid/pico/src/ph_cmds.c | 131 - hid/pico/src/ph_cmds.h | 39 - hid/pico/src/ph_com.c | 67 - hid/pico/src/ph_com.h | 30 - hid/pico/src/ph_com_bridge.c | 84 - hid/pico/src/ph_com_bridge.h | 30 - hid/pico/src/ph_com_spi.c | 121 - hid/pico/src/ph_com_spi.h | 30 - hid/pico/src/ph_com_uart.c | 75 - hid/pico/src/ph_com_uart.h | 30 - hid/pico/src/ph_debug.c | 58 - hid/pico/src/ph_debug.h | 31 - hid/pico/src/ph_outputs.c | 146 - hid/pico/src/ph_outputs.h | 46 - hid/pico/src/ph_proto.h | 92 - hid/pico/src/ph_ps2.c | 143 - hid/pico/src/ph_ps2.h | 49 - hid/pico/src/ph_tools.h | 57 - hid/pico/src/ph_types.h | 38 - hid/pico/src/ph_usb.c | 437 -- hid/pico/src/ph_usb.h | 43 - hid/pico/src/ph_usb_kbd.c | 78 - hid/pico/src/ph_usb_kbd.h | 29 - hid/pico/src/ph_usb_keymap.h | 143 - hid/pico/src/ph_usb_keymap.h.mako | 39 - hid/pico/src/ph_usb_mouse.c | 120 - hid/pico/src/ph_usb_mouse.h | 32 - hid/pico/src/tusb_config.h | 79 - kvmd/apps/kvmd/api/auth.py | 6 +- kvmd/apps/kvmd/api/export.py | 2 +- kvmd/apps/kvmd/api/hid.py | 20 +- kvmd/apps/kvmd/api/info.py | 64 +- kvmd/apps/kvmd/api/log.py | 2 +- kvmd/apps/kvmd/api/msd.py | 18 +- kvmd/apps/kvmd/api/redfish.py | 8 +- kvmd/apps/kvmd/api/streamer.py | 8 +- kvmd/apps/kvmd/server.py | 6 +- kvmd/clients/__init__.py | 16 +- kvmd/htserver.py | 19 +- .../etc/kvmd/.docker_flag | 0 .../etc/kvmd/.init_flag | 0 {configs => kvmd_data/etc}/kvmd/armbain-motd | 0 {configs => kvmd_data/etc}/kvmd/atx.sh | 0 {configs => kvmd_data/etc}/kvmd/auth.yaml | 0 .../etc}/kvmd/clean_when_exit.sh | 0 .../etc}/kvmd/custom_atx/gpio.sh | 0 .../etc}/kvmd/custom_atx/usbrelay_hid.py | 0 .../etc}/kvmd/custom_atx/usbrelay_hid.sh | 0 {configs => kvmd_data/etc}/kvmd/htpasswd | 0 {configs => kvmd_data/etc/kvmd}/hw_info/model | 0 .../etc/kvmd}/hw_info/serial-number | 0 {configs => kvmd_data/etc}/kvmd/ipmipasswd | 0 .../etc/kvmd}/janus/janus.jcfg | 0 .../kvmd}/janus/janus.plugin.ustreamer.jcfg | 0 .../janus/janus.transport.websockets.jcfg | 0 {configs => kvmd_data/etc}/kvmd/kvm_input.sh | 2 +- {configs => kvmd_data/etc}/kvmd/logging.yaml | 0 {configs => kvmd_data/etc}/kvmd/main.yaml | 0 .../etc}/kvmd/main/v4plus-hdmi-rpi4.yaml | 0 {configs => kvmd_data/etc}/kvmd/meta.yaml | 2 +- .../etc/kvmd}/nginx/certbot.ctx-server.conf | 2 +- kvmd_data/etc/kvmd/nginx/kvmd.ctx-http.conf | 7 + .../etc/kvmd}/nginx/kvmd.ctx-server.conf | 54 +- .../etc/kvmd}/nginx/loc-bigpost.conf | 0 .../etc/kvmd}/nginx/loc-login.conf | 0 .../etc/kvmd}/nginx/loc-nobuffering.conf | 0 .../etc/kvmd}/nginx/loc-nocache.conf | 0 .../etc/kvmd}/nginx/loc-proxy.conf | 0 .../etc/kvmd}/nginx/loc-websocket.conf | 0 .../etc/kvmd}/nginx/mime-types.conf | 0 kvmd_data/etc/kvmd/nginx/nginx.conf | 55 + .../etc/kvmd}/nginx/nginx.conf.mako | 0 .../etc/kvmd}/nginx/ssl.conf | 4 +- kvmd_data/etc/kvmd/nginx/ssl/server.crt | 12 + kvmd_data/etc/kvmd/nginx/ssl/server.key | 8 + {configs => kvmd_data/etc}/kvmd/override.yaml | 64 +- .../etc}/kvmd/supervisord.conf | 33 + .../etc/kvmd/totp.secret | 0 kvmd_data/etc/kvmd/vnc/ssl/server.crt | 12 + kvmd_data/etc/kvmd/vnc/ssl/server.key | 8 + {configs => kvmd_data/etc}/kvmd/vncpasswd | 0 {configs => kvmd_data/etc}/kvmd/web.css | 0 kvmd_data/usr/bin/ustreamer | Bin 0 -> 438592 bytes kvmd_data/usr/bin/vcgencmd | 4 + .../usr/share/kvmd/extras}/ipmi/manifest.yaml | 0 .../kvmd/extras}/janus-static/manifest.yaml | 0 .../share/kvmd/extras}/janus/manifest.yaml | 0 .../kvmd/extras}/janus/nginx.ctx-http.conf | 0 .../kvmd/extras}/janus/nginx.ctx-server.conf | 0 .../usr/share/kvmd/extras}/vnc/manifest.yaml | 0 .../share/kvmd/extras}/webterm/manifest.yaml | 0 .../kvmd/extras}/webterm/nginx.ctx-http.conf | 0 .../extras}/webterm/nginx.ctx-server.conf | 0 .../usr/share/kvmd}/keymaps/ar | 0 .../usr/share/kvmd}/keymaps/bepo | 0 .../usr/share/kvmd}/keymaps/cz | 0 .../usr/share/kvmd}/keymaps/da | 0 .../usr/share/kvmd}/keymaps/de | 0 .../usr/share/kvmd}/keymaps/de-ch | 0 .../usr/share/kvmd}/keymaps/en-gb | 0 .../usr/share/kvmd}/keymaps/en-us | 0 .../usr/share/kvmd}/keymaps/en-us-altgr-intl | 0 .../usr/share/kvmd}/keymaps/es | 0 .../usr/share/kvmd}/keymaps/et | 0 .../usr/share/kvmd}/keymaps/fi | 0 .../usr/share/kvmd}/keymaps/fo | 0 .../usr/share/kvmd}/keymaps/fr | 0 .../usr/share/kvmd}/keymaps/fr-be | 0 .../usr/share/kvmd}/keymaps/fr-ca | 0 .../usr/share/kvmd}/keymaps/fr-ch | 0 .../usr/share/kvmd}/keymaps/hr | 0 .../usr/share/kvmd}/keymaps/hu | 0 .../usr/share/kvmd}/keymaps/is | 0 .../usr/share/kvmd}/keymaps/it | 0 .../usr/share/kvmd}/keymaps/ja | 0 .../usr/share/kvmd}/keymaps/lt | 0 .../usr/share/kvmd}/keymaps/lv | 0 .../usr/share/kvmd}/keymaps/mk | 0 .../usr/share/kvmd}/keymaps/nl | 0 .../usr/share/kvmd}/keymaps/no | 0 .../usr/share/kvmd}/keymaps/pl | 0 .../usr/share/kvmd}/keymaps/pt | 0 .../usr/share/kvmd}/keymaps/pt-br | 0 .../usr/share/kvmd}/keymaps/ru | 0 .../usr/share/kvmd}/keymaps/sl | 0 .../usr/share/kvmd}/keymaps/sv | 0 .../usr/share/kvmd}/keymaps/th | 0 .../usr/share/kvmd}/keymaps/tr | 0 .../usr/share/kvmd}/kvmd-gencert | 0 kvmd_data/usr/share/kvmd/platform | 3 + .../usr/share/kvmd/web}/base.pug | 0 .../usr/share/kvmd/web/extras}/.gitignore | 0 .../kvmd/web}/extras/webterm/terminal.svg | 0 .../usr/share/kvmd/web}/favicon.ico | Bin .../usr/share/kvmd/web}/index.html | 0 .../usr/share/kvmd/web}/index.pug | 0 .../usr/share/kvmd/web}/ipmi/index.html | 0 .../usr/share/kvmd/web}/ipmi/index.pug | 0 .../usr/share/kvmd/web}/kvm/index.html | 0 .../usr/share/kvmd/web}/kvm/index.pug | 0 .../usr/share/kvmd/web}/kvm/navbar-atx.pug | 0 .../usr/share/kvmd/web}/kvm/navbar-gpio.pug | 0 .../usr/share/kvmd/web}/kvm/navbar-health.pug | 0 .../usr/share/kvmd/web}/kvm/navbar-macro.pug | 0 .../usr/share/kvmd/web}/kvm/navbar-msd.pug | 0 .../share/kvmd/web}/kvm/navbar-shortcuts.pug | 0 .../usr/share/kvmd/web}/kvm/navbar-system.pug | 0 .../usr/share/kvmd/web}/kvm/navbar-text.pug | 0 .../usr/share/kvmd/web}/kvm/navbar.pug | 0 .../usr/share/kvmd/web}/kvm/window-about.pug | 0 .../share/kvmd/web}/kvm/window-keyboard.pug | 0 .../usr/share/kvmd/web}/kvm/window-stream.pug | 0 .../share/kvmd/web}/kvm/window-webterm.pug | 0 .../usr/share/kvmd/web}/kvm/windows.pug | 0 .../usr/share/kvmd/web}/login/index.html | 0 .../usr/share/kvmd/web}/login/index.pug | 0 .../usr/share/kvmd/web}/robots.txt | 0 .../web}/share/android-chrome-192x192.png | Bin .../kvmd/web}/share/apple-touch-icon.png | Bin .../share/kvmd/web}/share/css/index/index.css | 0 .../usr/share/kvmd/web}/share/css/keypad.css | 0 .../share/kvmd/web}/share/css/kvm/about.css | 0 .../usr/share/kvmd/web}/share/css/kvm/hid.css | 0 .../kvmd/web}/share/css/kvm/keyboard.css | 0 .../usr/share/kvmd/web}/share/css/kvm/msd.css | 0 .../share/kvmd/web}/share/css/kvm/stream.css | 0 .../share/kvmd/web}/share/css/kvm/system.css | 0 .../kvmd/web}/share/css/kvm/x-mobile.css | 0 .../usr/share/kvmd/web}/share/css/led.css | 0 .../share/kvmd/web}/share/css/login/login.css | 0 .../usr/share/kvmd/web}/share/css/main.css | 0 .../usr/share/kvmd/web}/share/css/modal.css | 0 .../usr/share/kvmd/web}/share/css/navbar.css | 0 .../share/kvmd/web}/share/css/progress.css | 0 .../usr/share/kvmd/web}/share/css/radio.css | 0 .../usr/share/kvmd/web}/share/css/slider.css | 0 .../usr/share/kvmd/web}/share/css/start.css | 0 .../usr/share/kvmd/web}/share/css/switch.css | 0 .../usr/share/kvmd/web}/share/css/tabs.css | 0 .../usr/share/kvmd/web/share/css/user.css | 0 .../usr/share/kvmd/web}/share/css/vars.css | 0 .../usr/share/kvmd/web}/share/css/window.css | 0 .../share/kvmd/web}/share/css/x-desktop.css | 0 .../share/kvmd/web}/share/css/x-mobile.css | 0 .../share/kvmd/web}/share/favicon-16x16.png | Bin .../share/kvmd/web}/share/favicon-32x32.png | Bin .../share/kvmd/web}/share/i18n/i18n_en.json | 0 .../share/kvmd/web}/share/i18n/i18n_zh.json | 0 .../usr/share/kvmd/web}/share/js/bb.js | 0 .../usr/share/kvmd/web}/share/js/i18n/i18n.js | 0 .../web}/share/js/i18n/jquery-3.7.1.min.js | 0 .../web}/share/js/i18n/jquery.i18n.min.js | 0 .../share/kvmd/web}/share/js/index/main.js | 0 .../usr/share/kvmd/web}/share/js/ipmi/main.js | 0 .../usr/share/kvmd/web}/share/js/keypad.js | 0 .../share/kvmd/web/share/js/kvm/adapter.js | 3364 +++++++++++++++ .../usr/share/kvmd/web}/share/js/kvm/atx.js | 0 .../usr/share/kvmd/web}/share/js/kvm/gpio.js | 0 .../usr/share/kvmd/web}/share/js/kvm/hid.js | 0 .../usr/share/kvmd/web/share/js/kvm/janus.js | 3651 +++++++++++++++++ .../share/kvmd/web}/share/js/kvm/keyboard.js | 0 .../usr/share/kvmd/web}/share/js/kvm/main.js | 0 .../usr/share/kvmd/web}/share/js/kvm/mouse.js | 0 .../usr/share/kvmd/web}/share/js/kvm/msd.js | 0 .../usr/share/kvmd/web}/share/js/kvm/ocr.js | 0 .../usr/share/kvmd/web}/share/js/kvm/paste.js | 0 .../share/kvmd/web}/share/js/kvm/recorder.js | 0 .../share/kvmd/web}/share/js/kvm/session.js | 0 .../share/kvmd/web}/share/js/kvm/stream.js | 0 .../kvmd/web}/share/js/kvm/stream_janus.js | 0 .../kvmd/web}/share/js/kvm/stream_mjpeg.js | 1 + .../share/kvmd/web}/share/js/login/main.js | 0 .../usr/share/kvmd/web}/share/js/tools.js | 0 .../usr/share/kvmd/web}/share/js/vnc/main.js | 0 .../usr/share/kvmd/web}/share/js/wm.js | 0 .../kvmd/web}/share/png/blank-stream.png | Bin .../kvmd/web}/share/safari-pinned-tab.svg | 0 .../share/kvmd/web}/share/site.webmanifest | 0 .../usr/share/kvmd/web}/share/svg/favicon.svg | 0 .../usr/share/kvmd/web}/share/svg/info.svg | 0 .../usr/share/kvmd/web}/share/svg/ipmi.svg | 0 .../usr/share/kvmd/web}/share/svg/kvm.svg | 0 .../share/kvmd/web}/share/svg/led-atx-hdd.svg | 0 .../kvmd/web}/share/svg/led-atx-power.svg | 0 .../share/kvmd/web}/share/svg/led-circle.svg | 0 .../usr/share/kvmd/web}/share/svg/led-fan.svg | 0 .../share/kvmd/web}/share/svg/led-gear.svg | 0 .../kvmd/web}/share/svg/led-hid-keyboard.svg | 0 .../kvmd/web}/share/svg/led-hid-mouse.svg | 0 .../share/kvmd/web}/share/svg/led-link.svg | 0 .../share/kvmd/web}/share/svg/led-loading.svg | 0 .../usr/share/kvmd/web}/share/svg/led-msd.svg | 0 .../kvmd/web}/share/svg/led-overheating.svg | 0 .../share/kvmd/web}/share/svg/led-square.svg | 0 .../share/kvmd/web}/share/svg/led-stream.svg | 0 .../kvmd/web}/share/svg/led-undervoltage.svg | 0 .../usr/share/kvmd/web}/share/svg/logo.svg | 0 .../usr/share/kvmd/web}/share/svg/logout.svg | 0 .../web}/share/svg/select-arrow-inactive.svg | 0 .../web}/share/svg/select-arrow-intensive.svg | 0 .../web}/share/svg/select-arrow-normal.svg | 0 .../web}/share/svg/stream-mouse-cursor.svg | 0 .../usr/share/kvmd/web}/share/svg/vnc.svg | 0 .../usr/share/kvmd/web}/share/svg/warning.svg | 0 .../usr/share/kvmd/web}/start.pug | 0 .../usr/share/kvmd/web}/vnc/index.html | 0 .../usr/share/kvmd/web}/vnc/index.pug | 0 scripts/kvmd | 3 - scripts/kvmd-bootconfig | 292 -- scripts/kvmd-certbot | 157 - scripts/kvmd-htpasswd | 3 - scripts/kvmd-otg | 3 - scripts/kvmd-otgconf | 3 - scripts/kvmd-totp | 3 - scripts/kvmd-udev-hdmiusb-check | 56 - scripts/kvmd-udev-restart-pass | 49 - scripts/kvmd-vnc | 3 - web/extras/.gitignore | 0 411 files changed, 7387 insertions(+), 7263 deletions(-) delete mode 100644 .bumpversion.cfg delete mode 100644 .editorconfig delete mode 100644 configs/nginx/kvmd.ctx-http.conf delete mode 100644 configs/os/boot-config/v0-hdmi-rpi2.txt delete mode 100644 configs/os/boot-config/v0-hdmi-rpi3.txt delete mode 100644 configs/os/boot-config/v0-hdmi-zero2w.txt delete mode 100644 configs/os/boot-config/v0-hdmiusb-rpi2.txt delete mode 100644 configs/os/boot-config/v0-hdmiusb-rpi3.txt delete mode 100644 configs/os/boot-config/v0-hdmiusb-zero2w.txt delete mode 100644 configs/os/boot-config/v1-hdmi-rpi2.txt delete mode 100644 configs/os/boot-config/v1-hdmi-rpi3.txt delete mode 100644 configs/os/boot-config/v1-hdmi-zero2w.txt delete mode 100644 configs/os/boot-config/v1-hdmiusb-rpi2.txt delete mode 100644 configs/os/boot-config/v1-hdmiusb-rpi3.txt delete mode 100644 configs/os/boot-config/v1-hdmiusb-zero2w.txt delete mode 100644 configs/os/boot-config/v2-hdmi-rpi3.txt delete mode 100644 configs/os/boot-config/v2-hdmi-rpi4.txt delete mode 100644 configs/os/boot-config/v2-hdmi-zero2w.txt delete mode 100644 configs/os/boot-config/v2-hdmiusb-rpi4.txt delete mode 100644 configs/os/boot-config/v3-hdmi-rpi4.txt delete mode 100644 configs/os/boot-config/v4mini-hdmi-rpi4.txt delete mode 100644 configs/os/boot-config/v4plus-hdmi-rpi4.txt delete mode 100644 configs/os/cmdline/v0-hdmi-rpi2.sed delete mode 100644 configs/os/cmdline/v0-hdmi-rpi3.sed delete mode 100644 configs/os/cmdline/v0-hdmi-zero2w.sed delete mode 100644 configs/os/cmdline/v0-hdmiusb-rpi2.sed delete mode 100644 configs/os/cmdline/v0-hdmiusb-rpi3.sed delete mode 100644 configs/os/cmdline/v0-hdmiusb-zero2w.sed delete mode 100644 configs/os/cmdline/v1-hdmi-rpi2.sed delete mode 100644 configs/os/cmdline/v1-hdmi-rpi3.sed delete mode 100644 configs/os/cmdline/v1-hdmi-zero2w.sed delete mode 100644 configs/os/cmdline/v2-hdmi-rpi3.sed delete mode 100644 configs/os/cmdline/v2-hdmi-rpi4.sed delete mode 100644 configs/os/cmdline/v2-hdmi-zero2w.sed delete mode 100644 configs/os/cmdline/v2-hdmiusb-rpi4.sed delete mode 100644 configs/os/cmdline/v3-hdmi-rpi4.sed delete mode 100644 configs/os/cmdline/v4mini-hdmi-rpi4.sed delete mode 100644 configs/os/cmdline/v4plus-hdmi-rpi4.sed delete mode 100644 configs/os/kvmd-webterm.conf delete mode 100644 configs/os/modules-load/v0-hdmi.conf delete mode 100644 configs/os/modules-load/v1-hdmi.conf delete mode 100644 configs/os/modules-load/v2-hdmi.conf delete mode 100644 configs/os/modules-load/v2-hdmiusb.conf delete mode 100644 configs/os/modules-load/v3-hdmi.conf delete mode 100644 configs/os/modules-load/v4mini-hdmi.conf delete mode 100644 configs/os/modules-load/v4plus-hdmi.conf delete mode 100644 configs/os/services/kvmd-bootconfig.service delete mode 100644 configs/os/services/kvmd-certbot.service delete mode 100644 configs/os/services/kvmd-certbot.timer delete mode 100644 configs/os/services/kvmd-ipmi.service delete mode 100644 configs/os/services/kvmd-janus-static.service delete mode 100644 configs/os/services/kvmd-janus.service delete mode 100644 configs/os/services/kvmd-nginx.service delete mode 100644 configs/os/services/kvmd-oled-reboot.service delete mode 100644 configs/os/services/kvmd-oled-shutdown.service delete mode 100644 configs/os/services/kvmd-oled.service delete mode 100644 configs/os/services/kvmd-otg.service delete mode 100644 configs/os/services/kvmd-otgnet.service delete mode 100644 configs/os/services/kvmd-pst.service delete mode 100644 configs/os/services/kvmd-tc358743.service delete mode 100644 configs/os/services/kvmd-vnc.service delete mode 100644 configs/os/services/kvmd-watchdog.service delete mode 100644 configs/os/services/kvmd-webterm.service delete mode 100644 configs/os/services/kvmd.service delete mode 100644 configs/os/sudoers/v0-hdmi delete mode 100644 configs/os/sudoers/v0-hdmiusb delete mode 100644 configs/os/sudoers/v1-hdmi delete mode 100644 configs/os/sudoers/v1-hdmiusb delete mode 100644 configs/os/sudoers/v2-hdmi delete mode 100644 configs/os/sudoers/v2-hdmiusb delete mode 100644 configs/os/sudoers/v3-hdmi delete mode 100644 configs/os/sudoers/v4mini-hdmi delete mode 100644 configs/os/sudoers/v4plus-hdmi delete mode 100644 configs/os/sysctl.conf delete mode 100644 configs/os/sysusers.conf delete mode 100644 configs/os/tmpfiles.conf delete mode 100644 configs/os/udev/common.rules delete mode 100644 configs/os/udev/v0-hdmi-rpi2.rules delete mode 100644 configs/os/udev/v0-hdmi-rpi3.rules delete mode 100644 configs/os/udev/v0-hdmi-zero2w.rules delete mode 100644 configs/os/udev/v0-hdmiusb-rpi2.rules delete mode 100644 configs/os/udev/v0-hdmiusb-rpi3.rules delete mode 100644 configs/os/udev/v0-hdmiusb-zero2w.rules delete mode 100644 configs/os/udev/v1-hdmi-rpi2.rules delete mode 100644 configs/os/udev/v1-hdmi-rpi3.rules delete mode 100644 configs/os/udev/v1-hdmi-zero2w.rules delete mode 100644 configs/os/udev/v1-hdmiusb-rpi2.rules delete mode 100644 configs/os/udev/v1-hdmiusb-rpi3.rules delete mode 100644 configs/os/udev/v1-hdmiusb-zero2w.rules delete mode 100644 configs/os/udev/v2-hdmi-rpi3.rules delete mode 100644 configs/os/udev/v2-hdmi-rpi4.rules delete mode 100644 configs/os/udev/v2-hdmi-zero2w.rules delete mode 100644 configs/os/udev/v2-hdmiusb-generic.rules delete mode 100644 configs/os/udev/v2-hdmiusb-rpi4.rules delete mode 100644 configs/os/udev/v3-hdmi-rpi4.rules delete mode 100644 configs/os/udev/v4mini-hdmi-rpi4.rules delete mode 100644 configs/os/udev/v4plus-hdmi-rpi4.rules delete mode 100644 contrib/README delete mode 100644 hid/arduino/.gitignore delete mode 100644 hid/arduino/Makefile delete mode 100644 hid/arduino/avrdude-rpi.conf delete mode 100644 hid/arduino/avrdude.py delete mode 100644 hid/arduino/lib/drivers-avr/eeprom.h delete mode 100644 hid/arduino/lib/drivers-avr/factory.cpp delete mode 100644 hid/arduino/lib/drivers-avr/ps2/hid.h delete mode 100644 hid/arduino/lib/drivers-avr/ps2/keymap.h delete mode 100644 hid/arduino/lib/drivers-avr/ps2/keymap.h.mako delete mode 100644 hid/arduino/lib/drivers-avr/spi.cpp delete mode 100644 hid/arduino/lib/drivers-avr/spi.h delete mode 100644 hid/arduino/lib/drivers-avr/usb/hid.h delete mode 100644 hid/arduino/lib/drivers-stm32/README.md delete mode 100644 hid/arduino/lib/drivers-stm32/backup-register.h delete mode 100644 hid/arduino/lib/drivers-stm32/bluepill_sch.png delete mode 100644 hid/arduino/lib/drivers-stm32/board-stm32.h delete mode 100644 hid/arduino/lib/drivers-stm32/factory.cpp delete mode 100644 hid/arduino/lib/drivers-stm32/usb/hid-wrapper-stm32.h delete mode 100644 hid/arduino/lib/drivers-stm32/usb/keyboard-stm32.h delete mode 100644 hid/arduino/lib/drivers-stm32/usb/mouse-absolute-stm32.h delete mode 100644 hid/arduino/lib/drivers-stm32/usb/mouse-relative-stm32.h delete mode 100644 hid/arduino/lib/drivers/aum.h delete mode 100644 hid/arduino/lib/drivers/board.h delete mode 100644 hid/arduino/lib/drivers/connection.h delete mode 100644 hid/arduino/lib/drivers/driver.h delete mode 100644 hid/arduino/lib/drivers/factory.h delete mode 100644 hid/arduino/lib/drivers/keyboard.h delete mode 100644 hid/arduino/lib/drivers/mouse.h delete mode 100644 hid/arduino/lib/drivers/serial.h delete mode 100644 hid/arduino/lib/drivers/storage.h delete mode 100644 hid/arduino/lib/drivers/tools.cpp delete mode 100644 hid/arduino/lib/drivers/tools.h delete mode 100644 hid/arduino/lib/drivers/usb-keymap.h delete mode 100644 hid/arduino/lib/drivers/usb-keymap.h.mako delete mode 100644 hid/arduino/patch.py delete mode 100644 hid/arduino/patches/arduino-get-plugged-endpoint.patch delete mode 100644 hid/arduino/patches/arduino-main-no-usb.patch delete mode 100644 hid/arduino/patches/arduino-optional-cdc.patch delete mode 100644 hid/arduino/patches/hid-no-singletones.patch delete mode 100644 hid/arduino/patches/hid-shut-up.patch delete mode 100644 hid/arduino/patches/hid-win98.patch delete mode 100644 hid/arduino/patches/platformio-stm32f1-no-serial-usb.patch delete mode 100644 hid/arduino/platformio-avr.ini delete mode 100644 hid/arduino/platformio-stm32.ini delete mode 100644 hid/arduino/src/main.cpp delete mode 100644 hid/arduino/src/outputs.h delete mode 100644 hid/arduino/src/proto.h delete mode 100644 hid/pico/.gitignore delete mode 100644 hid/pico/CMakeLists.txt delete mode 100644 hid/pico/Makefile delete mode 100644 hid/pico/src/CMakeLists.txt delete mode 100644 hid/pico/src/main.c delete mode 100644 hid/pico/src/ph_cmds.c delete mode 100644 hid/pico/src/ph_cmds.h delete mode 100644 hid/pico/src/ph_com.c delete mode 100644 hid/pico/src/ph_com.h delete mode 100644 hid/pico/src/ph_com_bridge.c delete mode 100644 hid/pico/src/ph_com_bridge.h delete mode 100644 hid/pico/src/ph_com_spi.c delete mode 100644 hid/pico/src/ph_com_spi.h delete mode 100644 hid/pico/src/ph_com_uart.c delete mode 100644 hid/pico/src/ph_com_uart.h delete mode 100644 hid/pico/src/ph_debug.c delete mode 100644 hid/pico/src/ph_debug.h delete mode 100644 hid/pico/src/ph_outputs.c delete mode 100644 hid/pico/src/ph_outputs.h delete mode 100644 hid/pico/src/ph_proto.h delete mode 100644 hid/pico/src/ph_ps2.c delete mode 100644 hid/pico/src/ph_ps2.h delete mode 100644 hid/pico/src/ph_tools.h delete mode 100644 hid/pico/src/ph_types.h delete mode 100644 hid/pico/src/ph_usb.c delete mode 100644 hid/pico/src/ph_usb.h delete mode 100644 hid/pico/src/ph_usb_kbd.c delete mode 100644 hid/pico/src/ph_usb_kbd.h delete mode 100644 hid/pico/src/ph_usb_keymap.h delete mode 100644 hid/pico/src/ph_usb_keymap.h.mako delete mode 100644 hid/pico/src/ph_usb_mouse.c delete mode 100644 hid/pico/src/ph_usb_mouse.h delete mode 100644 hid/pico/src/tusb_config.h rename configs/kvmd/totp.secret => kvmd_data/etc/kvmd/.docker_flag (100%) rename configs/os/cmdline/v1-hdmiusb-rpi2.sed => kvmd_data/etc/kvmd/.init_flag (100%) rename {configs => kvmd_data/etc}/kvmd/armbain-motd (100%) rename {configs => kvmd_data/etc}/kvmd/atx.sh (100%) rename {configs => kvmd_data/etc}/kvmd/auth.yaml (100%) rename {configs => kvmd_data/etc}/kvmd/clean_when_exit.sh (100%) rename {configs => kvmd_data/etc}/kvmd/custom_atx/gpio.sh (100%) rename {configs => kvmd_data/etc}/kvmd/custom_atx/usbrelay_hid.py (100%) rename {configs => kvmd_data/etc}/kvmd/custom_atx/usbrelay_hid.sh (100%) rename {configs => kvmd_data/etc}/kvmd/htpasswd (100%) rename {configs => kvmd_data/etc/kvmd}/hw_info/model (100%) rename {configs => kvmd_data/etc/kvmd}/hw_info/serial-number (100%) rename {configs => kvmd_data/etc}/kvmd/ipmipasswd (100%) rename {configs => kvmd_data/etc/kvmd}/janus/janus.jcfg (100%) rename {configs => kvmd_data/etc/kvmd}/janus/janus.plugin.ustreamer.jcfg (100%) rename {configs => kvmd_data/etc/kvmd}/janus/janus.transport.websockets.jcfg (100%) rename {configs => kvmd_data/etc}/kvmd/kvm_input.sh (97%) rename {configs => kvmd_data/etc}/kvmd/logging.yaml (100%) rename {configs => kvmd_data/etc}/kvmd/main.yaml (100%) rename {configs => kvmd_data/etc}/kvmd/main/v4plus-hdmi-rpi4.yaml (100%) rename {configs => kvmd_data/etc}/kvmd/meta.yaml (91%) rename {configs => kvmd_data/etc/kvmd}/nginx/certbot.ctx-server.conf (68%) create mode 100644 kvmd_data/etc/kvmd/nginx/kvmd.ctx-http.conf rename {configs => kvmd_data/etc/kvmd}/nginx/kvmd.ctx-server.conf (63%) rename {configs => kvmd_data/etc/kvmd}/nginx/loc-bigpost.conf (100%) rename {configs => kvmd_data/etc/kvmd}/nginx/loc-login.conf (100%) rename {configs => kvmd_data/etc/kvmd}/nginx/loc-nobuffering.conf (100%) rename {configs => kvmd_data/etc/kvmd}/nginx/loc-nocache.conf (100%) rename {configs => kvmd_data/etc/kvmd}/nginx/loc-proxy.conf (100%) rename {configs => kvmd_data/etc/kvmd}/nginx/loc-websocket.conf (100%) rename {configs => kvmd_data/etc/kvmd}/nginx/mime-types.conf (100%) create mode 100644 kvmd_data/etc/kvmd/nginx/nginx.conf rename {configs => kvmd_data/etc/kvmd}/nginx/nginx.conf.mako (100%) rename {configs => kvmd_data/etc/kvmd}/nginx/ssl.conf (65%) create mode 100644 kvmd_data/etc/kvmd/nginx/ssl/server.crt create mode 100644 kvmd_data/etc/kvmd/nginx/ssl/server.key rename {configs => kvmd_data/etc}/kvmd/override.yaml (72%) rename {configs => kvmd_data/etc}/kvmd/supervisord.conf (61%) rename configs/os/cmdline/v1-hdmiusb-rpi3.sed => kvmd_data/etc/kvmd/totp.secret (100%) create mode 100644 kvmd_data/etc/kvmd/vnc/ssl/server.crt create mode 100644 kvmd_data/etc/kvmd/vnc/ssl/server.key rename {configs => kvmd_data/etc}/kvmd/vncpasswd (100%) rename {configs => kvmd_data/etc}/kvmd/web.css (100%) create mode 100755 kvmd_data/usr/bin/ustreamer create mode 100755 kvmd_data/usr/bin/vcgencmd rename {extras => kvmd_data/usr/share/kvmd/extras}/ipmi/manifest.yaml (100%) rename {extras => kvmd_data/usr/share/kvmd/extras}/janus-static/manifest.yaml (100%) rename {extras => kvmd_data/usr/share/kvmd/extras}/janus/manifest.yaml (100%) rename {extras => kvmd_data/usr/share/kvmd/extras}/janus/nginx.ctx-http.conf (100%) rename {extras => kvmd_data/usr/share/kvmd/extras}/janus/nginx.ctx-server.conf (100%) rename {extras => kvmd_data/usr/share/kvmd/extras}/vnc/manifest.yaml (100%) rename {extras => kvmd_data/usr/share/kvmd/extras}/webterm/manifest.yaml (100%) rename {extras => kvmd_data/usr/share/kvmd/extras}/webterm/nginx.ctx-http.conf (100%) rename {extras => kvmd_data/usr/share/kvmd/extras}/webterm/nginx.ctx-server.conf (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/ar (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/bepo (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/cz (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/da (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/de (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/de-ch (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/en-gb (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/en-us (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/en-us-altgr-intl (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/es (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/et (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/fi (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/fo (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/fr (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/fr-be (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/fr-ca (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/fr-ch (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/hr (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/hu (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/is (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/it (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/ja (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/lt (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/lv (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/mk (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/nl (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/no (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/pl (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/pt (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/pt-br (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/ru (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/sl (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/sv (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/th (100%) rename {contrib => kvmd_data/usr/share/kvmd}/keymaps/tr (100%) rename {scripts => kvmd_data/usr/share/kvmd}/kvmd-gencert (100%) create mode 100644 kvmd_data/usr/share/kvmd/platform rename {web => kvmd_data/usr/share/kvmd/web}/base.pug (100%) rename {hid/arduino/lib => kvmd_data/usr/share/kvmd/web/extras}/.gitignore (100%) rename {web => kvmd_data/usr/share/kvmd/web}/extras/webterm/terminal.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/favicon.ico (100%) rename {web => kvmd_data/usr/share/kvmd/web}/index.html (100%) rename {web => kvmd_data/usr/share/kvmd/web}/index.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/ipmi/index.html (100%) rename {web => kvmd_data/usr/share/kvmd/web}/ipmi/index.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/index.html (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/index.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/navbar-atx.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/navbar-gpio.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/navbar-health.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/navbar-macro.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/navbar-msd.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/navbar-shortcuts.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/navbar-system.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/navbar-text.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/navbar.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/window-about.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/window-keyboard.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/window-stream.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/window-webterm.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/kvm/windows.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/login/index.html (100%) rename {web => kvmd_data/usr/share/kvmd/web}/login/index.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/robots.txt (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/android-chrome-192x192.png (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/apple-touch-icon.png (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/index/index.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/keypad.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/kvm/about.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/kvm/hid.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/kvm/keyboard.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/kvm/msd.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/kvm/stream.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/kvm/system.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/kvm/x-mobile.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/led.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/login/login.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/main.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/modal.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/navbar.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/progress.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/radio.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/slider.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/start.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/switch.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/tabs.css (100%) rename configs/os/cmdline/v1-hdmiusb-zero2w.sed => kvmd_data/usr/share/kvmd/web/share/css/user.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/vars.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/window.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/x-desktop.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/css/x-mobile.css (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/favicon-16x16.png (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/favicon-32x32.png (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/i18n/i18n_en.json (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/i18n/i18n_zh.json (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/bb.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/i18n/i18n.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/i18n/jquery-3.7.1.min.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/i18n/jquery.i18n.min.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/index/main.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/ipmi/main.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/keypad.js (100%) create mode 100644 kvmd_data/usr/share/kvmd/web/share/js/kvm/adapter.js rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/atx.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/gpio.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/hid.js (100%) create mode 100644 kvmd_data/usr/share/kvmd/web/share/js/kvm/janus.js rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/keyboard.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/main.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/mouse.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/msd.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/ocr.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/paste.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/recorder.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/session.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/stream.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/stream_janus.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/kvm/stream_mjpeg.js (99%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/login/main.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/tools.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/vnc/main.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/js/wm.js (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/png/blank-stream.png (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/safari-pinned-tab.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/site.webmanifest (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/favicon.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/info.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/ipmi.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/kvm.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-atx-hdd.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-atx-power.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-circle.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-fan.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-gear.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-hid-keyboard.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-hid-mouse.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-link.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-loading.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-msd.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-overheating.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-square.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-stream.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/led-undervoltage.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/logo.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/logout.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/select-arrow-inactive.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/select-arrow-intensive.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/select-arrow-normal.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/stream-mouse-cursor.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/vnc.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/share/svg/warning.svg (100%) rename {web => kvmd_data/usr/share/kvmd/web}/start.pug (100%) rename {web => kvmd_data/usr/share/kvmd/web}/vnc/index.html (100%) rename {web => kvmd_data/usr/share/kvmd/web}/vnc/index.pug (100%) delete mode 100755 scripts/kvmd delete mode 100755 scripts/kvmd-bootconfig delete mode 100755 scripts/kvmd-certbot delete mode 100755 scripts/kvmd-htpasswd delete mode 100755 scripts/kvmd-otg delete mode 100755 scripts/kvmd-otgconf delete mode 100755 scripts/kvmd-totp delete mode 100755 scripts/kvmd-udev-hdmiusb-check delete mode 100755 scripts/kvmd-udev-restart-pass delete mode 100755 scripts/kvmd-vnc delete mode 100644 web/extras/.gitignore diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 926b4e31..00000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[bumpversion] -commit = True -tag = True -current_version = 4.20 -parse = (?P\d+)\.(?P\d+)(\.(?P\d+)(\-(?P[a-z]+))?)? -serialize = - {major}.{minor} - -[bumpversion:file:kvmd/__init__.py] -search = __version__ = "{current_version}" -replace = __version__ = "{new_version}" - -[bumpversion:file:setup.py] -search = version="{current_version}" -replace = version="{new_version}" - -[bumpversion:file:PKGBUILD] -search = pkgver={current_version} -replace = pkgver={new_version} diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 447d3a8d..00000000 --- a/.editorconfig +++ /dev/null @@ -1,10 +0,0 @@ -root = true - -[*] -end_of_file = lf -indent_style = tab -indent_size = 4 - -[*.{py,yaml}] -indent_style = space -indent_size = 4 diff --git a/.gitignore b/.gitignore index e3fcca5e..96074115 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ /venv/ .vscode/settings.j/son kvmd_config/ +__pycache__/ +kvmd_data/run/kvmd/* +.vscode/ diff --git a/Makefile b/Makefile index ab47094f..b4c6cc73 100644 --- a/Makefile +++ b/Makefile @@ -294,6 +294,13 @@ run-build-dev: -f build/Dockerfile . \ --push +run-build-new: + $(DOCKER) buildx build -t registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd:dev \ + --platform linux/amd64 \ + --build-arg CACHEBUST=$(date +%s) \ + -f build/Dockerfile . \ + --load + run-build-release: $(DOCKER) buildx build -t registry.cn-hangzhou.aliyuncs.com/silentwind/kvmd \ --progress plain \ @@ -342,4 +349,18 @@ run-nogpio: testenv && python -m kvmd.apps.ngxmkconf /etc/kvmd/nginx/nginx.conf.mako /etc/kvmd/nginx/nginx.conf \ && nginx -c /etc/kvmd/nginx/nginx.conf -g 'user http; error_log stderr;' \ && $(if $(CMD),$(CMD),python -m kvmd.apps.kvmd --run) \ - " \ No newline at end of file + " +nuitka: + python -m nuitka kvmd-test.py --standalone --onefile --no-deployment-flag=self-execution --include-module=\ + kvmd.plugins.auth.htpasswd,kvmd.plugins.auth.http,kvmd.plugins.auth.ldap,\ + kvmd.plugins.auth.pam,kvmd.plugins.auth.radius,\ + kvmd.plugins.hid.ch9329,kvmd.plugins.hid.bt,kvmd.plugins.hid.otg,\ + kvmd.plugins.atx.disabled,kvmd.plugins.atx.gpio,\ + kvmd.plugins.msd.disabled,kvmd.plugins.msd.otg,\ + kvmd.plugins.ugpio.gpio,kvmd.plugins.ugpio.wol,kvmd.plugins.ugpio.cmd,\ + kvmd.plugins.ugpio.ipmi,kvmd.plugins.ugpio.anelpwr,kvmd.plugins.ugpio.cmdret,\ + kvmd.plugins.ugpio.extron,kvmd.plugins.ugpio.ezcoo,kvmd.plugins.ugpio.hidrelay,\ + kvmd.plugins.ugpio.hue,kvmd.plugins.ugpio.locator,kvmd.plugins.ugpio.noyito,\ + kvmd.plugins.ugpio.otgconf,kvmd.plugins.ugpio.pway,kvmd.plugins.ugpio.pwm,\ + kvmd.plugins.ugpio.servo,kvmd.plugins.ugpio.tesmart,kvmd.plugins.ugpio.xh_hk4401,\ + passlib.handlers.sha1_crypt,pygments.formatters.terminal \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile index 77b279a6..d87119a0 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -30,23 +30,9 @@ RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/' /etc/apt/sources.lis RUN if [ ${TARGETARCH} = arm ]; then ARCH=armhf; elif [ ${TARGETARCH} = arm64 ]; then ARCH=aarch64; elif [ ${TARGETARCH} = amd64 ]; then ARCH=x86_64; fi \ && curl https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.$ARCH -L -o /usr/local/bin/ttyd \ && chmod +x /usr/local/bin/ttyd \ - && adduser kvmd --gecos "" --disabled-password \ - && ln -sf /usr/share/tesseract-ocr/*/tessdata /usr/share/tessdata \ - && mkdir -p /etc/kvmd_backup/override.d /var/lib/kvmd/msd/images /var/lib/kvmd/msd/meta /var/lib/kvmd/pst/data /var/lib/kvmd/msd/NormalFiles /opt/vc/bin /run/kvmd /tmp/kvmd-nginx \ - && touch /run/kvmd/ustreamer.sock + && ln -sf /usr/share/tesseract-ocr/*/tessdata /usr/share/tessdata -COPY testenv/fakes/vcgencmd scripts/kvmd* /usr/bin/ -COPY extras/ /usr/share/kvmd/extras/ -COPY web/ /usr/share/kvmd/web/ -COPY scripts/kvmd-gencert /usr/share/kvmd/ -COPY build/platform/docker /usr/share/kvmd/platform -COPY contrib/keymaps /usr/share/kvmd/keymaps -COPY kvmd/ build/init.sh /kvmd/ -COPY configs/kvmd/ /etc/kvmd_backup/ -COPY configs/nginx/ /etc/kvmd_backup/nginx/ -COPY configs/janus/ /etc/kvmd_backup/janus/ -COPY configs/hw_info/ /etc/kvmd_backup/hw_info/ -COPY testenv/js/ /usr/share/janus/javascript/ +COPY . /One-KVM -ENTRYPOINT ["/kvmd/init.sh"] \ No newline at end of file +ENTRYPOINT ["bash"] \ No newline at end of file diff --git a/configs/nginx/kvmd.ctx-http.conf b/configs/nginx/kvmd.ctx-http.conf deleted file mode 100644 index ceb68ef8..00000000 --- a/configs/nginx/kvmd.ctx-http.conf +++ /dev/null @@ -1,7 +0,0 @@ -upstream kvmd { - server unix:/run/kvmd/kvmd.sock fail_timeout=0s max_fails=0; -} - -upstream ustreamer { - server unix:/run/kvmd/ustreamer.sock fail_timeout=0s max_fails=0; -} diff --git a/configs/os/boot-config/v0-hdmi-rpi2.txt b/configs/os/boot-config/v0-hdmi-rpi2.txt deleted file mode 100644 index 853673e9..00000000 --- a/configs/os/boot-config/v0-hdmi-rpi2.txt +++ /dev/null @@ -1,7 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=128 -enable_uart=1 -dtoverlay=tc358743,i2c_pins_28_29=1 diff --git a/configs/os/boot-config/v0-hdmi-rpi3.txt b/configs/os/boot-config/v0-hdmi-rpi3.txt deleted file mode 100644 index f9c43c3b..00000000 --- a/configs/os/boot-config/v0-hdmi-rpi3.txt +++ /dev/null @@ -1,8 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=128 -enable_uart=1 -dtoverlay=tc358743 -dtoverlay=disable-bt diff --git a/configs/os/boot-config/v0-hdmi-zero2w.txt b/configs/os/boot-config/v0-hdmi-zero2w.txt deleted file mode 100644 index 54459566..00000000 --- a/configs/os/boot-config/v0-hdmi-zero2w.txt +++ /dev/null @@ -1,8 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=96 -enable_uart=1 -dtoverlay=tc358743,i2c_pins_28_29=1 -dtoverlay=disable-bt diff --git a/configs/os/boot-config/v0-hdmiusb-rpi2.txt b/configs/os/boot-config/v0-hdmiusb-rpi2.txt deleted file mode 100644 index d78de4ae..00000000 --- a/configs/os/boot-config/v0-hdmiusb-rpi2.txt +++ /dev/null @@ -1,6 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=16 -enable_uart=1 diff --git a/configs/os/boot-config/v0-hdmiusb-rpi3.txt b/configs/os/boot-config/v0-hdmiusb-rpi3.txt deleted file mode 100644 index 5d41ab3c..00000000 --- a/configs/os/boot-config/v0-hdmiusb-rpi3.txt +++ /dev/null @@ -1,7 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=16 -enable_uart=1 -dtoverlay=disable-bt diff --git a/configs/os/boot-config/v0-hdmiusb-zero2w.txt b/configs/os/boot-config/v0-hdmiusb-zero2w.txt deleted file mode 100644 index 5d41ab3c..00000000 --- a/configs/os/boot-config/v0-hdmiusb-zero2w.txt +++ /dev/null @@ -1,7 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=16 -enable_uart=1 -dtoverlay=disable-bt diff --git a/configs/os/boot-config/v1-hdmi-rpi2.txt b/configs/os/boot-config/v1-hdmi-rpi2.txt deleted file mode 100644 index e94b342d..00000000 --- a/configs/os/boot-config/v1-hdmi-rpi2.txt +++ /dev/null @@ -1,8 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=128 -enable_uart=1 -dtoverlay=tc358743,i2c_pins_28_29=1 -dtoverlay=spi0-1cs diff --git a/configs/os/boot-config/v1-hdmi-rpi3.txt b/configs/os/boot-config/v1-hdmi-rpi3.txt deleted file mode 100644 index ad44d95c..00000000 --- a/configs/os/boot-config/v1-hdmi-rpi3.txt +++ /dev/null @@ -1,9 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=128 -enable_uart=1 -dtoverlay=tc358743 -dtoverlay=disable-bt -dtoverlay=spi0-1cs diff --git a/configs/os/boot-config/v1-hdmi-zero2w.txt b/configs/os/boot-config/v1-hdmi-zero2w.txt deleted file mode 100644 index 0a1e9009..00000000 --- a/configs/os/boot-config/v1-hdmi-zero2w.txt +++ /dev/null @@ -1,9 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=96 -enable_uart=1 -dtoverlay=tc358743,i2c_pins_28_29=1 -dtoverlay=disable-bt -dtoverlay=spi0-1cs diff --git a/configs/os/boot-config/v1-hdmiusb-rpi2.txt b/configs/os/boot-config/v1-hdmiusb-rpi2.txt deleted file mode 100644 index 9957f1a4..00000000 --- a/configs/os/boot-config/v1-hdmiusb-rpi2.txt +++ /dev/null @@ -1,7 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=16 -enable_uart=1 -dtoverlay=spi0-1cs diff --git a/configs/os/boot-config/v1-hdmiusb-rpi3.txt b/configs/os/boot-config/v1-hdmiusb-rpi3.txt deleted file mode 100644 index fe4ce032..00000000 --- a/configs/os/boot-config/v1-hdmiusb-rpi3.txt +++ /dev/null @@ -1,8 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=16 -enable_uart=1 -dtoverlay=disable-bt -dtoverlay=spi0-1cs diff --git a/configs/os/boot-config/v1-hdmiusb-zero2w.txt b/configs/os/boot-config/v1-hdmiusb-zero2w.txt deleted file mode 100644 index fe4ce032..00000000 --- a/configs/os/boot-config/v1-hdmiusb-zero2w.txt +++ /dev/null @@ -1,8 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=16 -enable_uart=1 -dtoverlay=disable-bt -dtoverlay=spi0-1cs diff --git a/configs/os/boot-config/v2-hdmi-rpi3.txt b/configs/os/boot-config/v2-hdmi-rpi3.txt deleted file mode 100644 index 7a2f75b4..00000000 --- a/configs/os/boot-config/v2-hdmi-rpi3.txt +++ /dev/null @@ -1,9 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=128 -enable_uart=1 -dtoverlay=tc358743 -dtoverlay=disable-bt -dtoverlay=dwc2,dr_mode=peripheral diff --git a/configs/os/boot-config/v2-hdmi-rpi4.txt b/configs/os/boot-config/v2-hdmi-rpi4.txt deleted file mode 100644 index 7a2f75b4..00000000 --- a/configs/os/boot-config/v2-hdmi-rpi4.txt +++ /dev/null @@ -1,9 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=128 -enable_uart=1 -dtoverlay=tc358743 -dtoverlay=disable-bt -dtoverlay=dwc2,dr_mode=peripheral diff --git a/configs/os/boot-config/v2-hdmi-zero2w.txt b/configs/os/boot-config/v2-hdmi-zero2w.txt deleted file mode 100644 index 6ec3dc54..00000000 --- a/configs/os/boot-config/v2-hdmi-zero2w.txt +++ /dev/null @@ -1,9 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=96 -enable_uart=1 -dtoverlay=tc358743,i2c_pins_28_29=1 -dtoverlay=disable-bt -dtoverlay=dwc2,dr_mode=peripheral diff --git a/configs/os/boot-config/v2-hdmiusb-rpi4.txt b/configs/os/boot-config/v2-hdmiusb-rpi4.txt deleted file mode 100644 index a53ba19c..00000000 --- a/configs/os/boot-config/v2-hdmiusb-rpi4.txt +++ /dev/null @@ -1,8 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=16 -enable_uart=1 -dtoverlay=disable-bt -dtoverlay=dwc2,dr_mode=peripheral diff --git a/configs/os/boot-config/v3-hdmi-rpi4.txt b/configs/os/boot-config/v3-hdmi-rpi4.txt deleted file mode 100644 index e9ec5038..00000000 --- a/configs/os/boot-config/v3-hdmi-rpi4.txt +++ /dev/null @@ -1,22 +0,0 @@ -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=128 -enable_uart=1 -dtoverlay=tc358743 -dtoverlay=disable-bt -dtoverlay=dwc2,dr_mode=peripheral -dtparam=act_led_gpio=13 - -# HDMI audio capture -dtoverlay=tc358743-audio - -# SPI (AUM) -dtoverlay=spi0-1cs - -# I2C (display) -dtparam=i2c_arm=on - -# Clock -dtoverlay=i2c-rtc,pcf8563,wakeup-source diff --git a/configs/os/boot-config/v4mini-hdmi-rpi4.txt b/configs/os/boot-config/v4mini-hdmi-rpi4.txt deleted file mode 100644 index 7664187d..00000000 --- a/configs/os/boot-config/v4mini-hdmi-rpi4.txt +++ /dev/null @@ -1,21 +0,0 @@ -# PiKVM -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=128 -enable_uart=1 -dtoverlay=disable-bt - -# USB emulation -dtoverlay=dwc2,dr_mode=peripheral - -# Video and audio -dtoverlay=tc358743,4lane=1 -dtoverlay=tc358743-audio - -# I2C (display) -dtparam=i2c_arm=on - -# Clock -dtoverlay=i2c-rtc,pcf8563,wakeup-source diff --git a/configs/os/boot-config/v4plus-hdmi-rpi4.txt b/configs/os/boot-config/v4plus-hdmi-rpi4.txt deleted file mode 100644 index 05821ea4..00000000 --- a/configs/os/boot-config/v4plus-hdmi-rpi4.txt +++ /dev/null @@ -1,29 +0,0 @@ -# PiKVM -# See /boot/overlays/README for all available options -initramfs initramfs-linux.img followkernel - -hdmi_force_hotplug=1 -gpu_mem=128 -enable_uart=1 -dtoverlay=disable-bt - -# USB emulation -dtoverlay=dwc2,dr_mode=peripheral - -# Video and audio -dtoverlay=tc358743,4lane=1 -dtoverlay=tc358743-audio - -# Passthrough -dtoverlay=vc4-kms-v3d -disable_overscan=1 - -# I2C (display) -dtparam=i2c_arm=on - -# Clock -dtoverlay=i2c-rtc,pcf8563,wakeup-source - -# Passthrough -dtoverlay=vc4-kms-v3d -disable_overscan=1 diff --git a/configs/os/cmdline/v0-hdmi-rpi2.sed b/configs/os/cmdline/v0-hdmi-rpi2.sed deleted file mode 100644 index 3089ad26..00000000 --- a/configs/os/cmdline/v0-hdmi-rpi2.sed +++ /dev/null @@ -1,5 +0,0 @@ -s/console=ttyAMA0\,115200//g -s/kgdboc=ttyAMA0\,115200//g -s/console=serial0\,115200//g -s/kgdboc=serial0\,115200//g -s/rootwait/rootwait cma=128M/g diff --git a/configs/os/cmdline/v0-hdmi-rpi3.sed b/configs/os/cmdline/v0-hdmi-rpi3.sed deleted file mode 100644 index 3089ad26..00000000 --- a/configs/os/cmdline/v0-hdmi-rpi3.sed +++ /dev/null @@ -1,5 +0,0 @@ -s/console=ttyAMA0\,115200//g -s/kgdboc=ttyAMA0\,115200//g -s/console=serial0\,115200//g -s/kgdboc=serial0\,115200//g -s/rootwait/rootwait cma=128M/g diff --git a/configs/os/cmdline/v0-hdmi-zero2w.sed b/configs/os/cmdline/v0-hdmi-zero2w.sed deleted file mode 100644 index ef88f019..00000000 --- a/configs/os/cmdline/v0-hdmi-zero2w.sed +++ /dev/null @@ -1,5 +0,0 @@ -s/console=ttyAMA0\,115200//g -s/kgdboc=ttyAMA0\,115200//g -s/console=serial0\,115200//g -s/kgdboc=serial0\,115200//g -s/rootwait/rootwait cma=96M/g diff --git a/configs/os/cmdline/v0-hdmiusb-rpi2.sed b/configs/os/cmdline/v0-hdmiusb-rpi2.sed deleted file mode 100644 index d2ac8b1e..00000000 --- a/configs/os/cmdline/v0-hdmiusb-rpi2.sed +++ /dev/null @@ -1,4 +0,0 @@ -s/console=ttyAMA0\,115200//g -s/kgdboc=ttyAMA0\,115200//g -s/console=serial0\,115200//g -s/kgdboc=serial0\,115200//g diff --git a/configs/os/cmdline/v0-hdmiusb-rpi3.sed b/configs/os/cmdline/v0-hdmiusb-rpi3.sed deleted file mode 100644 index d2ac8b1e..00000000 --- a/configs/os/cmdline/v0-hdmiusb-rpi3.sed +++ /dev/null @@ -1,4 +0,0 @@ -s/console=ttyAMA0\,115200//g -s/kgdboc=ttyAMA0\,115200//g -s/console=serial0\,115200//g -s/kgdboc=serial0\,115200//g diff --git a/configs/os/cmdline/v0-hdmiusb-zero2w.sed b/configs/os/cmdline/v0-hdmiusb-zero2w.sed deleted file mode 100644 index d2ac8b1e..00000000 --- a/configs/os/cmdline/v0-hdmiusb-zero2w.sed +++ /dev/null @@ -1,4 +0,0 @@ -s/console=ttyAMA0\,115200//g -s/kgdboc=ttyAMA0\,115200//g -s/console=serial0\,115200//g -s/kgdboc=serial0\,115200//g diff --git a/configs/os/cmdline/v1-hdmi-rpi2.sed b/configs/os/cmdline/v1-hdmi-rpi2.sed deleted file mode 100644 index ee1a5540..00000000 --- a/configs/os/cmdline/v1-hdmi-rpi2.sed +++ /dev/null @@ -1 +0,0 @@ -s/rootwait/rootwait cma=128M/g diff --git a/configs/os/cmdline/v1-hdmi-rpi3.sed b/configs/os/cmdline/v1-hdmi-rpi3.sed deleted file mode 100644 index ee1a5540..00000000 --- a/configs/os/cmdline/v1-hdmi-rpi3.sed +++ /dev/null @@ -1 +0,0 @@ -s/rootwait/rootwait cma=128M/g diff --git a/configs/os/cmdline/v1-hdmi-zero2w.sed b/configs/os/cmdline/v1-hdmi-zero2w.sed deleted file mode 100644 index 9c551c25..00000000 --- a/configs/os/cmdline/v1-hdmi-zero2w.sed +++ /dev/null @@ -1 +0,0 @@ -s/rootwait/rootwait cma=96M/g diff --git a/configs/os/cmdline/v2-hdmi-rpi3.sed b/configs/os/cmdline/v2-hdmi-rpi3.sed deleted file mode 100644 index ee1a5540..00000000 --- a/configs/os/cmdline/v2-hdmi-rpi3.sed +++ /dev/null @@ -1 +0,0 @@ -s/rootwait/rootwait cma=128M/g diff --git a/configs/os/cmdline/v2-hdmi-rpi4.sed b/configs/os/cmdline/v2-hdmi-rpi4.sed deleted file mode 100644 index ee1a5540..00000000 --- a/configs/os/cmdline/v2-hdmi-rpi4.sed +++ /dev/null @@ -1 +0,0 @@ -s/rootwait/rootwait cma=128M/g diff --git a/configs/os/cmdline/v2-hdmi-zero2w.sed b/configs/os/cmdline/v2-hdmi-zero2w.sed deleted file mode 100644 index 9c551c25..00000000 --- a/configs/os/cmdline/v2-hdmi-zero2w.sed +++ /dev/null @@ -1 +0,0 @@ -s/rootwait/rootwait cma=96M/g diff --git a/configs/os/cmdline/v2-hdmiusb-rpi4.sed b/configs/os/cmdline/v2-hdmiusb-rpi4.sed deleted file mode 100644 index e69de29b..00000000 diff --git a/configs/os/cmdline/v3-hdmi-rpi4.sed b/configs/os/cmdline/v3-hdmi-rpi4.sed deleted file mode 100644 index ee1a5540..00000000 --- a/configs/os/cmdline/v3-hdmi-rpi4.sed +++ /dev/null @@ -1 +0,0 @@ -s/rootwait/rootwait cma=128M/g diff --git a/configs/os/cmdline/v4mini-hdmi-rpi4.sed b/configs/os/cmdline/v4mini-hdmi-rpi4.sed deleted file mode 100644 index ee1a5540..00000000 --- a/configs/os/cmdline/v4mini-hdmi-rpi4.sed +++ /dev/null @@ -1 +0,0 @@ -s/rootwait/rootwait cma=128M/g diff --git a/configs/os/cmdline/v4plus-hdmi-rpi4.sed b/configs/os/cmdline/v4plus-hdmi-rpi4.sed deleted file mode 100644 index ee1a5540..00000000 --- a/configs/os/cmdline/v4plus-hdmi-rpi4.sed +++ /dev/null @@ -1 +0,0 @@ -s/rootwait/rootwait cma=128M/g diff --git a/configs/os/kvmd-webterm.conf b/configs/os/kvmd-webterm.conf deleted file mode 100644 index 3a015fb3..00000000 --- a/configs/os/kvmd-webterm.conf +++ /dev/null @@ -1,6 +0,0 @@ -g kvmd-webterm - - - -u kvmd-webterm - "Pi-KVM - Web terminal" /home/kvmd-webterm - -m kvmd-webterm kvmd -m kvmd-nginx kvmd-webterm diff --git a/configs/os/modules-load/v0-hdmi.conf b/configs/os/modules-load/v0-hdmi.conf deleted file mode 100644 index 5be25408..00000000 --- a/configs/os/modules-load/v0-hdmi.conf +++ /dev/null @@ -1 +0,0 @@ -tc358743 diff --git a/configs/os/modules-load/v1-hdmi.conf b/configs/os/modules-load/v1-hdmi.conf deleted file mode 100644 index 5be25408..00000000 --- a/configs/os/modules-load/v1-hdmi.conf +++ /dev/null @@ -1 +0,0 @@ -tc358743 diff --git a/configs/os/modules-load/v2-hdmi.conf b/configs/os/modules-load/v2-hdmi.conf deleted file mode 100644 index af538dba..00000000 --- a/configs/os/modules-load/v2-hdmi.conf +++ /dev/null @@ -1,3 +0,0 @@ -dwc2 -libcomposite -tc358743 diff --git a/configs/os/modules-load/v2-hdmiusb.conf b/configs/os/modules-load/v2-hdmiusb.conf deleted file mode 100644 index 9c9626be..00000000 --- a/configs/os/modules-load/v2-hdmiusb.conf +++ /dev/null @@ -1,2 +0,0 @@ -dwc2 -libcomposite diff --git a/configs/os/modules-load/v3-hdmi.conf b/configs/os/modules-load/v3-hdmi.conf deleted file mode 100644 index f1ede9d1..00000000 --- a/configs/os/modules-load/v3-hdmi.conf +++ /dev/null @@ -1,4 +0,0 @@ -dwc2 -libcomposite -tc358743 -i2c-dev diff --git a/configs/os/modules-load/v4mini-hdmi.conf b/configs/os/modules-load/v4mini-hdmi.conf deleted file mode 100644 index f1ede9d1..00000000 --- a/configs/os/modules-load/v4mini-hdmi.conf +++ /dev/null @@ -1,4 +0,0 @@ -dwc2 -libcomposite -tc358743 -i2c-dev diff --git a/configs/os/modules-load/v4plus-hdmi.conf b/configs/os/modules-load/v4plus-hdmi.conf deleted file mode 100644 index f1ede9d1..00000000 --- a/configs/os/modules-load/v4plus-hdmi.conf +++ /dev/null @@ -1,4 +0,0 @@ -dwc2 -libcomposite -tc358743 -i2c-dev diff --git a/configs/os/services/kvmd-bootconfig.service b/configs/os/services/kvmd-bootconfig.service deleted file mode 100644 index ba5469b9..00000000 --- a/configs/os/services/kvmd-bootconfig.service +++ /dev/null @@ -1,29 +0,0 @@ -[Unit] -Description=PiKVM - Boot configuration -After=systemd-modules-load.service kvmd-oled.service -Before=\ - kvmd-webterm.service \ - kvmd-certbot.service \ - kvmd-ipmi.service \ - kvmd-janus-static.service \ - kvmd-janus.service \ - kvmd-nginx.service \ - kvmd-otg.service \ - kvmd-otgnet.service \ - kvmd-pst.service \ - kvmd-tc358743.service \ - kvmd-vnc.service \ - kvmd-watchdog.service \ - kvmd.service \ - pikvm-bootconfig.service \ - sshd.service \ - network-pre.target - -[Service] -Type=oneshot -ExecStart=/usr/bin/kvmd-bootconfig --do-the-thing -ExecStop=/bin/true -RemainAfterExit=true - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-certbot.service b/configs/os/services/kvmd-certbot.service deleted file mode 100644 index 03515b5c..00000000 --- a/configs/os/services/kvmd-certbot.service +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=PiKVM - Certbot-Renew for KVMD-Nginx - -[Service] -Type=oneshot -EnvironmentFile=-/etc/conf.d/kvmd-certbot -ExecStart=/usr/bin/kvmd-certbot renew --quiet -# Should we?.. -# PrivateTmp=true diff --git a/configs/os/services/kvmd-certbot.timer b/configs/os/services/kvmd-certbot.timer deleted file mode 100644 index 84fc9fcd..00000000 --- a/configs/os/services/kvmd-certbot.timer +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -Description=Run KVMD-Certbot twice daily - -[Timer] -OnCalendar=*-*-* 00/12:00:00 -RandomizedDelaySec=12h -Persistent=true - -[Install] -WantedBy=timers.target diff --git a/configs/os/services/kvmd-ipmi.service b/configs/os/services/kvmd-ipmi.service deleted file mode 100644 index 17bcc2b9..00000000 --- a/configs/os/services/kvmd-ipmi.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=One-KVM - IPMI to KVMD proxy -After=kvmd.service - -[Service] -User=kvmd-ipmi -Group=kvmd-ipmi -Type=simple -Restart=always -RestartSec=3 -AmbientCapabilities=CAP_NET_BIND_SERVICE - -ExecStart=/usr/bin/kvmd-ipmi --run -TimeoutStopSec=3 - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-janus-static.service b/configs/os/services/kvmd-janus-static.service deleted file mode 100644 index 15ed723b..00000000 --- a/configs/os/services/kvmd-janus-static.service +++ /dev/null @@ -1,22 +0,0 @@ -[Unit] -Description=PiKVM - Janus WebRTC Gateway (Static Config) -After=network.target network-online.target nss-lookup.target kvmd.service - -[Service] -User=kvmd-janus -Group=kvmd-janus -Type=simple -Restart=always -RestartSec=3 -AmbientCapabilities=CAP_NET_RAW -LimitNOFILE=65536 - -# Crutch for UNIX socket perms -UMask=0117 - -ExecStart=/usr/bin/janus --disable-colors --plugins-folder=/usr/lib/ustreamer/janus --configs-folder=/etc/kvmd/janus -TimeoutStopSec=10 -KillMode=mixed - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-janus.service b/configs/os/services/kvmd-janus.service deleted file mode 100644 index ee36835b..00000000 --- a/configs/os/services/kvmd-janus.service +++ /dev/null @@ -1,22 +0,0 @@ -[Unit] -Description=One-KVM - Janus WebRTC Gateway -After=network.target network-online.target nss-lookup.target kvmd.service - -[Service] -User=kvmd-janus -Group=kvmd-janus -Type=simple -Restart=always -RestartSec=3 -AmbientCapabilities=CAP_NET_RAW -LimitNOFILE=65536 - -# Crutch for UNIX socket perms -UMask=0117 - -ExecStart=/usr/bin/kvmd-janus --run -TimeoutStopSec=10 -KillMode=mixed - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-nginx.service b/configs/os/services/kvmd-nginx.service deleted file mode 100644 index 0189fd5f..00000000 --- a/configs/os/services/kvmd-nginx.service +++ /dev/null @@ -1,21 +0,0 @@ -[Unit] -Description=One-KVM - HTTP entrypoint -After=network.target network-online.target nss-lookup.target kvmd.service - -[Service] -Type=forking -PIDFile=/run/kvmd/nginx.pid -PrivateDevices=yes -SyslogLevel=err -Restart=always -RestartSec=3 - -ExecStartPre=/usr/bin/kvmd-nginx-mkconf /etc/kvmd/nginx/nginx.conf.mako /run/kvmd/nginx.conf -ExecStart=/usr/sbin/nginx -p /etc/kvmd/nginx -c /run/kvmd/nginx.conf -g 'pid /run/kvmd/nginx.pid; user kvmd-nginx; error_log stderr;' -ExecReload=/usr/sbin/nginx -s reload -p /etc/kvmd/nginx -c /run/kvmd/nginx.conf -g 'pid /run/kvmd/nginx.pid; user kvmd-nginx; error_log stderr;' -KillSignal=SIGQUIT -KillMode=mixed -TimeoutStopSec=3 - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-oled-reboot.service b/configs/os/services/kvmd-oled-reboot.service deleted file mode 100644 index 23bbca15..00000000 --- a/configs/os/services/kvmd-oled-reboot.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=PiKVM - Display reboot message on the OLED -DefaultDependencies=no - -[Service] -Type=oneshot -ExecStart=/bin/bash -c "kill -USR1 `systemctl show -P MainPID kvmd-oled`" -ExecStop=/bin/true -RemainAfterExit=yes - -[Install] -WantedBy=reboot.target diff --git a/configs/os/services/kvmd-oled-shutdown.service b/configs/os/services/kvmd-oled-shutdown.service deleted file mode 100644 index 61d94c51..00000000 --- a/configs/os/services/kvmd-oled-shutdown.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=PiKVM - Display shutdown message on the OLED -Conflicts=reboot.target -Before=shutdown.target poweroff.target halt.target -DefaultDependencies=no - -[Service] -Type=oneshot -ExecStart=/bin/bash -c "kill -USR2 `systemctl show -P MainPID kvmd-oled`" -ExecStop=/bin/true -RemainAfterExit=yes - -[Install] -WantedBy=shutdown.target diff --git a/configs/os/services/kvmd-oled.service b/configs/os/services/kvmd-oled.service deleted file mode 100644 index ea0d4850..00000000 --- a/configs/os/services/kvmd-oled.service +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=PiKVM - A small OLED daemon -After=systemd-modules-load.service -ConditionPathExists=/dev/i2c-1 - -[Service] -Type=simple -Restart=always -RestartSec=3 -ExecStartPre=/usr/bin/kvmd-oled --interval=3 --clear-on-exit --image=@hello.ppm -ExecStart=/usr/bin/kvmd-oled -TimeoutStopSec=3 - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-otg.service b/configs/os/services/kvmd-otg.service deleted file mode 100644 index c4209779..00000000 --- a/configs/os/services/kvmd-otg.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=One-KVM- OTG setup -After=systemd-modules-load.service -Before=kvmd.service - -[Service] -Type=oneshot -ExecStart=/usr/bin/kvmd-otg start -ExecStop=/usr/bin/kvmd-otg stop -RemainAfterExit=true - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-otgnet.service b/configs/os/services/kvmd-otgnet.service deleted file mode 100644 index 693c9bba..00000000 --- a/configs/os/services/kvmd-otgnet.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=PiKVM - OTG network service -After=kvmd-otg.service -Wants=network-pre.target -After=network-pre.target - -[Service] -Type=oneshot -ExecStart=/usr/bin/kvmd-otgnet start -ExecStop=/usr/bin/kvmd-otgnet stop -RemainAfterExit=true - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-pst.service b/configs/os/services/kvmd-pst.service deleted file mode 100644 index 9d77c3a0..00000000 --- a/configs/os/services/kvmd-pst.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=PiKVM - The KVMD persistent storage manager -Before=kvmd.service - -[Service] -User=kvmd-pst -Group=kvmd-pst -Type=simple -Restart=always -RestartSec=3 - -ExecStart=/usr/bin/kvmd-pst --run -TimeoutStopSec=5 - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-tc358743.service b/configs/os/services/kvmd-tc358743.service deleted file mode 100644 index 494da726..00000000 --- a/configs/os/services/kvmd-tc358743.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=PiKVM - EDID loader for TC358743 -Wants=dev-kvmd\x2dvideo.device -After=dev-kvmd\x2dvideo.device systemd-modules-load.service -Before=kvmd.service - -[Service] -Type=oneshot -ExecStart=/usr/bin/v4l2-ctl --device=/dev/kvmd-video --set-edid=file=/etc/kvmd/tc358743-edid.hex --info-edid -ExecStop=/usr/bin/v4l2-ctl --device=/dev/kvmd-video --clear-edid -RemainAfterExit=true - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-vnc.service b/configs/os/services/kvmd-vnc.service deleted file mode 100644 index f04b295b..00000000 --- a/configs/os/services/kvmd-vnc.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=One-KVM - VNC to KVMD/Streamer proxy -After=kvmd.service - -[Service] -User=kvmd-vnc -Group=kvmd-vnc -Type=simple -Restart=always -RestartSec=3 - -ExecStart=/usr/bin/kvmd-vnc --run -TimeoutStopSec=3 - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-watchdog.service b/configs/os/services/kvmd-watchdog.service deleted file mode 100644 index b6f4c45e..00000000 --- a/configs/os/services/kvmd-watchdog.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=PiKVM - RTC-based hardware watchdog -After=systemd-modules-load.service - -[Service] -Type=simple -Restart=always -RestartSec=3 - -ExecStart=/usr/bin/kvmd-watchdog run -TimeoutStopSec=3 - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd-webterm.service b/configs/os/services/kvmd-webterm.service deleted file mode 100644 index ca2089c5..00000000 --- a/configs/os/services/kvmd-webterm.service +++ /dev/null @@ -1,24 +0,0 @@ -[Unit] -Description=One-KVM - Web terminal (ttyd) -After=network.target - -[Service] -User=kvmd-webterm -Group=kvmd-webterm -WorkingDirectory=/home/kvmd-webterm -Restart=always -RestartSec=1 - -# Crutch for UNIX socket perms -UMask=0117 - -EnvironmentFile=-/etc/conf.d/kvmd-webterm -ExecStart=/usr/bin/ttyd \ - -W \ - --interface=/run/kvmd/ttyd.sock \ - --port=0 \ - $KVMD_WEBTERM_ARGS \ - /bin/bash -c 'echo -ne "\033]0;One-KVM Terminal: `hostname -f` (ttyd)\007"; bash /etc/kvmd/armbain-motd; export TERM=linux; umask 0022; bash' - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/services/kvmd.service b/configs/os/services/kvmd.service deleted file mode 100644 index 65b04bb7..00000000 --- a/configs/os/services/kvmd.service +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=One-KVM - The main daemon -After=network.target network-online.target nss-lookup.target - -[Service] -User=kvmd -Group=kvmd -Type=simple -Restart=always -RestartSec=3 -AmbientCapabilities=CAP_NET_RAW - -ExecStart=/usr/bin/kvmd --run -TimeoutStopSec=10 -KillMode=mixed - -[Install] -WantedBy=multi-user.target diff --git a/configs/os/sudoers/v0-hdmi b/configs/os/sudoers/v0-hdmi deleted file mode 100644 index bb549dc8..00000000 --- a/configs/os/sudoers/v0-hdmi +++ /dev/null @@ -1 +0,0 @@ -kvmd-pst ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-pst-remount diff --git a/configs/os/sudoers/v0-hdmiusb b/configs/os/sudoers/v0-hdmiusb deleted file mode 100644 index bb549dc8..00000000 --- a/configs/os/sudoers/v0-hdmiusb +++ /dev/null @@ -1 +0,0 @@ -kvmd-pst ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-pst-remount diff --git a/configs/os/sudoers/v1-hdmi b/configs/os/sudoers/v1-hdmi deleted file mode 100644 index bb549dc8..00000000 --- a/configs/os/sudoers/v1-hdmi +++ /dev/null @@ -1 +0,0 @@ -kvmd-pst ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-pst-remount diff --git a/configs/os/sudoers/v1-hdmiusb b/configs/os/sudoers/v1-hdmiusb deleted file mode 100644 index bb549dc8..00000000 --- a/configs/os/sudoers/v1-hdmiusb +++ /dev/null @@ -1 +0,0 @@ -kvmd-pst ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-pst-remount diff --git a/configs/os/sudoers/v2-hdmi b/configs/os/sudoers/v2-hdmi deleted file mode 100644 index 673d4033..00000000 --- a/configs/os/sudoers/v2-hdmi +++ /dev/null @@ -1,2 +0,0 @@ -kvmd ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-otgmsd-remount -kvmd-pst ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-pst-remount diff --git a/configs/os/sudoers/v2-hdmiusb b/configs/os/sudoers/v2-hdmiusb deleted file mode 100644 index 673d4033..00000000 --- a/configs/os/sudoers/v2-hdmiusb +++ /dev/null @@ -1,2 +0,0 @@ -kvmd ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-otgmsd-remount -kvmd-pst ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-pst-remount diff --git a/configs/os/sudoers/v3-hdmi b/configs/os/sudoers/v3-hdmi deleted file mode 100644 index 673d4033..00000000 --- a/configs/os/sudoers/v3-hdmi +++ /dev/null @@ -1,2 +0,0 @@ -kvmd ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-otgmsd-remount -kvmd-pst ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-pst-remount diff --git a/configs/os/sudoers/v4mini-hdmi b/configs/os/sudoers/v4mini-hdmi deleted file mode 100644 index 673d4033..00000000 --- a/configs/os/sudoers/v4mini-hdmi +++ /dev/null @@ -1,2 +0,0 @@ -kvmd ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-otgmsd-remount -kvmd-pst ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-pst-remount diff --git a/configs/os/sudoers/v4plus-hdmi b/configs/os/sudoers/v4plus-hdmi deleted file mode 100644 index 673d4033..00000000 --- a/configs/os/sudoers/v4plus-hdmi +++ /dev/null @@ -1,2 +0,0 @@ -kvmd ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-otgmsd-remount -kvmd-pst ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-pst-remount diff --git a/configs/os/sysctl.conf b/configs/os/sysctl.conf deleted file mode 100644 index abe53aa6..00000000 --- a/configs/os/sysctl.conf +++ /dev/null @@ -1,10 +0,0 @@ -# Overly-aggressive data writeback policies to minimize the chance of data corruption -# Start dirty data writeback when it exceeds 256 KiB or 1s has passed -vm.dirty_background_bytes = 262144 -vm.dirty_writeback_centisecs = 100 - -# https://groups.google.com/g/meetecho-janus/c/xoWIQfaoJm8 -net.core.rmem_default = 500000 -net.core.wmem_default = 500000 -net.core.rmem_max = 1000000 -net.core.wmem_max = 1000000 diff --git a/configs/os/sysusers.conf b/configs/os/sysusers.conf deleted file mode 100644 index 0359974d..00000000 --- a/configs/os/sysusers.conf +++ /dev/null @@ -1,38 +0,0 @@ -g kvmd - - -g kvmd-pst - - -g kvmd-ipmi - - -g kvmd-vnc - - -g kvmd-nginx - - -g kvmd-janus - - -g kvmd-certbot - - - -u kvmd - "PiKVM - The main daemon" - -u kvmd-pst - "PiKVM - Persistent storage" - -u kvmd-ipmi - "PiKVM - IPMI to KVMD proxy" - -u kvmd-vnc - "PiKVM - VNC to KVMD/Streamer proxy" - -u kvmd-nginx - "PiKVM - HTTP entrypoint" - -u kvmd-janus - "PiKVM - Janus WebRTC Gateway" - -u kvmd-certbot - "PiKVM - Certbot-Renew for KVMD-Nginx" - -m kvmd video -m kvmd gpio -m kvmd uucp -m kvmd spi -m kvmd systemd-journal -m kvmd kvmd-pst - -m kvmd-pst kvmd - -m kvmd-ipmi kvmd - -m kvmd-vnc kvmd -m kvmd-vnc kvmd-certbot - -m kvmd-janus kvmd -m kvmd-janus audio - -m kvmd-nginx kvmd -m kvmd-nginx kvmd-janus -m kvmd-nginx kvmd-certbot - -m kvmd-certbot kvmd-pst diff --git a/configs/os/tmpfiles.conf b/configs/os/tmpfiles.conf deleted file mode 100644 index 973c5d43..00000000 --- a/configs/os/tmpfiles.conf +++ /dev/null @@ -1,7 +0,0 @@ -D /run/kvmd 0775 kvmd kvmd - -D /run/kvmd-certbot 0755 root root - -D /run/kvmd-certbot/webroot 0755 kvmd-certbot kvmd-certbot - - -D /tmp/kvmd 0775 kvmd kvmd - -D /tmp/kvmd-nginx 0700 kvmd-nginx root - -D /tmp/kvmd-certbot 0755 kvmd-certbot kvmd-certbot - diff --git a/configs/os/udev/common.rules b/configs/os/udev/common.rules deleted file mode 100644 index 1a0ccded..00000000 --- a/configs/os/udev/common.rules +++ /dev/null @@ -1,3 +0,0 @@ -# Here are described some bindings for PiKVM devices. -# Do not edit this file. -KERNEL=="ttyACM[0-9]*", SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="eda3", SYMLINK+="kvmd-hid-bridge" diff --git a/configs/os/udev/v0-hdmi-rpi2.rules b/configs/os/udev/v0-hdmi-rpi2.rules deleted file mode 100644 index e60aa798..00000000 --- a/configs/os/udev/v0-hdmi-rpi2.rules +++ /dev/null @@ -1,4 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", KERNELS=="3f801000.csi|3f801000.csi1", ATTR{name}=="unicam-image", GROUP="kvmd", SYMLINK+="kvmd-video", TAG+="systemd" -KERNEL=="ttyAMA0", SYMLINK+="kvmd-hid" diff --git a/configs/os/udev/v0-hdmi-rpi3.rules b/configs/os/udev/v0-hdmi-rpi3.rules deleted file mode 100644 index e60aa798..00000000 --- a/configs/os/udev/v0-hdmi-rpi3.rules +++ /dev/null @@ -1,4 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", KERNELS=="3f801000.csi|3f801000.csi1", ATTR{name}=="unicam-image", GROUP="kvmd", SYMLINK+="kvmd-video", TAG+="systemd" -KERNEL=="ttyAMA0", SYMLINK+="kvmd-hid" diff --git a/configs/os/udev/v0-hdmi-zero2w.rules b/configs/os/udev/v0-hdmi-zero2w.rules deleted file mode 100644 index e60aa798..00000000 --- a/configs/os/udev/v0-hdmi-zero2w.rules +++ /dev/null @@ -1,4 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", KERNELS=="3f801000.csi|3f801000.csi1", ATTR{name}=="unicam-image", GROUP="kvmd", SYMLINK+="kvmd-video", TAG+="systemd" -KERNEL=="ttyAMA0", SYMLINK+="kvmd-hid" diff --git a/configs/os/udev/v0-hdmiusb-rpi2.rules b/configs/os/udev/v0-hdmiusb-rpi2.rules deleted file mode 100644 index 244f7ce4..00000000 --- a/configs/os/udev/v0-hdmiusb-rpi2.rules +++ /dev/null @@ -1,4 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", PROGRAM="/usr/bin/kvmd-udev-hdmiusb-check rpi2 %b", ATTR{index}=="0", GROUP="kvmd", SYMLINK+="kvmd-video" -KERNEL=="ttyAMA0", SYMLINK+="kvmd-hid" diff --git a/configs/os/udev/v0-hdmiusb-rpi3.rules b/configs/os/udev/v0-hdmiusb-rpi3.rules deleted file mode 100644 index bd96d232..00000000 --- a/configs/os/udev/v0-hdmiusb-rpi3.rules +++ /dev/null @@ -1,4 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", PROGRAM="/usr/bin/kvmd-udev-hdmiusb-check rpi3 %b", ATTR{index}=="0", GROUP="kvmd", SYMLINK+="kvmd-video" -KERNEL=="ttyAMA0", SYMLINK+="kvmd-hid" diff --git a/configs/os/udev/v0-hdmiusb-zero2w.rules b/configs/os/udev/v0-hdmiusb-zero2w.rules deleted file mode 100644 index 933f7337..00000000 --- a/configs/os/udev/v0-hdmiusb-zero2w.rules +++ /dev/null @@ -1,4 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video0", SUBSYSTEM=="video4linux", GROUP="kvmd", SYMLINK+="kvmd-video" -KERNEL=="ttyAMA0", SYMLINK+="kvmd-hid" diff --git a/configs/os/udev/v1-hdmi-rpi2.rules b/configs/os/udev/v1-hdmi-rpi2.rules deleted file mode 100644 index 49ca9ce0..00000000 --- a/configs/os/udev/v1-hdmi-rpi2.rules +++ /dev/null @@ -1,3 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", KERNELS=="3f801000.csi|3f801000.csi1", ATTR{name}=="unicam-image", GROUP="kvmd", SYMLINK+="kvmd-video", TAG+="systemd" diff --git a/configs/os/udev/v1-hdmi-rpi3.rules b/configs/os/udev/v1-hdmi-rpi3.rules deleted file mode 100644 index 49ca9ce0..00000000 --- a/configs/os/udev/v1-hdmi-rpi3.rules +++ /dev/null @@ -1,3 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", KERNELS=="3f801000.csi|3f801000.csi1", ATTR{name}=="unicam-image", GROUP="kvmd", SYMLINK+="kvmd-video", TAG+="systemd" diff --git a/configs/os/udev/v1-hdmi-zero2w.rules b/configs/os/udev/v1-hdmi-zero2w.rules deleted file mode 100644 index 49ca9ce0..00000000 --- a/configs/os/udev/v1-hdmi-zero2w.rules +++ /dev/null @@ -1,3 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", KERNELS=="3f801000.csi|3f801000.csi1", ATTR{name}=="unicam-image", GROUP="kvmd", SYMLINK+="kvmd-video", TAG+="systemd" diff --git a/configs/os/udev/v1-hdmiusb-rpi2.rules b/configs/os/udev/v1-hdmiusb-rpi2.rules deleted file mode 100644 index eafd8ef8..00000000 --- a/configs/os/udev/v1-hdmiusb-rpi2.rules +++ /dev/null @@ -1,3 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", PROGRAM="/usr/bin/kvmd-udev-hdmiusb-check rpi2 %b", ATTR{index}=="0", GROUP="kvmd", SYMLINK+="kvmd-video" diff --git a/configs/os/udev/v1-hdmiusb-rpi3.rules b/configs/os/udev/v1-hdmiusb-rpi3.rules deleted file mode 100644 index f28bdfae..00000000 --- a/configs/os/udev/v1-hdmiusb-rpi3.rules +++ /dev/null @@ -1,3 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", PROGRAM="/usr/bin/kvmd-udev-hdmiusb-check rpi3 %b", ATTR{index}=="0", GROUP="kvmd", SYMLINK+="kvmd-video" diff --git a/configs/os/udev/v1-hdmiusb-zero2w.rules b/configs/os/udev/v1-hdmiusb-zero2w.rules deleted file mode 100644 index 1a2a6f7d..00000000 --- a/configs/os/udev/v1-hdmiusb-zero2w.rules +++ /dev/null @@ -1,3 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video0", SUBSYSTEM=="video4linux", GROUP="kvmd", SYMLINK+="kvmd-video" diff --git a/configs/os/udev/v2-hdmi-rpi3.rules b/configs/os/udev/v2-hdmi-rpi3.rules deleted file mode 100644 index 6c6a6d3e..00000000 --- a/configs/os/udev/v2-hdmi-rpi3.rules +++ /dev/null @@ -1,6 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", KERNELS=="3f801000.csi|3f801000.csi1", ATTR{name}=="unicam-image", GROUP="kvmd", SYMLINK+="kvmd-video", TAG+="systemd" -KERNEL=="hidg0", GROUP="kvmd", SYMLINK+="kvmd-hid-keyboard" -KERNEL=="hidg1", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse" -KERNEL=="hidg2", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse-alt" diff --git a/configs/os/udev/v2-hdmi-rpi4.rules b/configs/os/udev/v2-hdmi-rpi4.rules deleted file mode 100644 index 88ba66b1..00000000 --- a/configs/os/udev/v2-hdmi-rpi4.rules +++ /dev/null @@ -1,6 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", KERNELS=="fe801000.csi|fe801000.csi1", ATTR{name}=="unicam-image", GROUP="kvmd", SYMLINK+="kvmd-video", TAG+="systemd" -KERNEL=="hidg0", GROUP="kvmd", SYMLINK+="kvmd-hid-keyboard" -KERNEL=="hidg1", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse" -KERNEL=="hidg2", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse-alt" diff --git a/configs/os/udev/v2-hdmi-zero2w.rules b/configs/os/udev/v2-hdmi-zero2w.rules deleted file mode 100644 index 6c6a6d3e..00000000 --- a/configs/os/udev/v2-hdmi-zero2w.rules +++ /dev/null @@ -1,6 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", KERNELS=="3f801000.csi|3f801000.csi1", ATTR{name}=="unicam-image", GROUP="kvmd", SYMLINK+="kvmd-video", TAG+="systemd" -KERNEL=="hidg0", GROUP="kvmd", SYMLINK+="kvmd-hid-keyboard" -KERNEL=="hidg1", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse" -KERNEL=="hidg2", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse-alt" diff --git a/configs/os/udev/v2-hdmiusb-generic.rules b/configs/os/udev/v2-hdmiusb-generic.rules deleted file mode 100644 index 0865f227..00000000 --- a/configs/os/udev/v2-hdmiusb-generic.rules +++ /dev/null @@ -1,7 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", SUBSYSTEMS=="usb", ATTR{index}=="0", GROUP="kvmd", SYMLINK+="kvmd-video" -KERNEL=="hidg0", GROUP="kvmd", SYMLINK+="kvmd-hid-keyboard" -KERNEL=="hidg1", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse" -KERNEL=="hidg2", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse-alt" -KERNEL=="ttyUSB0", GROUP="kvmd", SYMLINK+="kvmd-hid" diff --git a/configs/os/udev/v2-hdmiusb-rpi4.rules b/configs/os/udev/v2-hdmiusb-rpi4.rules deleted file mode 100644 index 2bab50be..00000000 --- a/configs/os/udev/v2-hdmiusb-rpi4.rules +++ /dev/null @@ -1,6 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", PROGRAM="/usr/bin/kvmd-udev-hdmiusb-check rpi4 %b", ATTR{index}=="0", GROUP="kvmd", SYMLINK+="kvmd-video" -KERNEL=="hidg0", GROUP="kvmd", SYMLINK+="kvmd-hid-keyboard" -KERNEL=="hidg1", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse" -KERNEL=="hidg2", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse-alt" diff --git a/configs/os/udev/v3-hdmi-rpi4.rules b/configs/os/udev/v3-hdmi-rpi4.rules deleted file mode 100644 index 88ba66b1..00000000 --- a/configs/os/udev/v3-hdmi-rpi4.rules +++ /dev/null @@ -1,6 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", KERNELS=="fe801000.csi|fe801000.csi1", ATTR{name}=="unicam-image", GROUP="kvmd", SYMLINK+="kvmd-video", TAG+="systemd" -KERNEL=="hidg0", GROUP="kvmd", SYMLINK+="kvmd-hid-keyboard" -KERNEL=="hidg1", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse" -KERNEL=="hidg2", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse-alt" diff --git a/configs/os/udev/v4mini-hdmi-rpi4.rules b/configs/os/udev/v4mini-hdmi-rpi4.rules deleted file mode 100644 index 88ba66b1..00000000 --- a/configs/os/udev/v4mini-hdmi-rpi4.rules +++ /dev/null @@ -1,6 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", KERNELS=="fe801000.csi|fe801000.csi1", ATTR{name}=="unicam-image", GROUP="kvmd", SYMLINK+="kvmd-video", TAG+="systemd" -KERNEL=="hidg0", GROUP="kvmd", SYMLINK+="kvmd-hid-keyboard" -KERNEL=="hidg1", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse" -KERNEL=="hidg2", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse-alt" diff --git a/configs/os/udev/v4plus-hdmi-rpi4.rules b/configs/os/udev/v4plus-hdmi-rpi4.rules deleted file mode 100644 index 87cfc700..00000000 --- a/configs/os/udev/v4plus-hdmi-rpi4.rules +++ /dev/null @@ -1,7 +0,0 @@ -# https://unix.stackexchange.com/questions/66901/how-to-bind-usb-device-under-a-static-name -# https://wiki.archlinux.org/index.php/Udev#Setting_static_device_names -KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", KERNELS=="fe801000.csi|fe801000.csi1", ATTR{name}=="unicam-image", GROUP="kvmd", SYMLINK+="kvmd-video", TAG+="systemd" -KERNEL=="hidg0", GROUP="kvmd", SYMLINK+="kvmd-hid-keyboard" -KERNEL=="hidg1", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse" -KERNEL=="hidg2", GROUP="kvmd", SYMLINK+="kvmd-hid-mouse-alt" -SUBSYSTEM=="drm", ACTION=="change", ENV{DEVLINKS}=="/dev/dri/by-path/platform-gpu-card", RUN+="/usr/bin/kvmd-udev-restart-pass %k %E{CONNECTOR}" diff --git a/contrib/README b/contrib/README deleted file mode 100644 index 4a479412..00000000 --- a/contrib/README +++ /dev/null @@ -1,3 +0,0 @@ -* keymaps: - - https://github.com/qemu/qemu/blob/master/pc-bios/keymaps - - https://github.com/j0xaf/keymap-us-altgr-intl/blob/main/keymap-us-altgr-intl diff --git a/hid/arduino/.gitignore b/hid/arduino/.gitignore deleted file mode 100644 index 05b843bc..00000000 --- a/hid/arduino/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/.platformio/ -/.pio/ -/.current -/.vscode/ -/.config -/platformio.ini diff --git a/hid/arduino/Makefile b/hid/arduino/Makefile deleted file mode 100644 index 652ddd6e..00000000 --- a/hid/arduino/Makefile +++ /dev/null @@ -1,51 +0,0 @@ -serial: - make _build E=serial C=avr -spi: - make _build E=spi C=avr -aum: - make _build E=aum C=avr -stm32: - platformio run --environment patch --project-conf platformio-stm32.ini - make _build E=serial C=stm32 -_build: - rm -f .current .config - platformio run --environment $(E) --project-conf platformio-$(C).ini - echo -n $(E) > .current - echo -n $(C) > .config - -# Added to easy test all builds -_build_all: aum spi serial stm32 - rm -f .current .config - -install: upload -upload: - $(eval $@_CURRENT := $(shell cat .current)) - $(eval $@_CONFIG := $(shell cat .config)) - bash -ex -c " \ - current=`cat .current`; \ - if [ '$($@_CURRENT)' == 'spi' ] || [ '$($@_CURRENT)' == 'aum' ]; then \ - gpioset 0 25=1; \ - gpioset 0 25=0; \ - fi \ - " - platformio run --environment '$($@_CURRENT)' --project-conf 'platformio-$($@_CONFIG).ini' --target upload - - -bootloader-spi: install-bootloader-spi -install-bootloader-spi: upload-bootloader-spi -upload-bootloader-spi: - platformio run --environment bootloader_spi --project-conf platformio-avr.ini --target bootloader - - -update: - platformio platform update - - -clean-all: clean - rm -rf .platformio -clean: - rm -rf .pio .current .config platformio.ini - - -help: - @ cat Makefile diff --git a/hid/arduino/avrdude-rpi.conf b/hid/arduino/avrdude-rpi.conf deleted file mode 100644 index 8a6f5460..00000000 --- a/hid/arduino/avrdude-rpi.conf +++ /dev/null @@ -1,7 +0,0 @@ -programmer - id = "rpi"; - desc = "RPi SPI programmer"; - type = "linuxspi"; - reset = 25; - baudrate = 400000; -; diff --git a/hid/arduino/avrdude.py b/hid/arduino/avrdude.py deleted file mode 100644 index 3ae227f9..00000000 --- a/hid/arduino/avrdude.py +++ /dev/null @@ -1,54 +0,0 @@ -# https://docs.platformio.org/en/latest/projectconf/advanced_scripting.html - - -from os import rename -from os import symlink -from os.path import exists -from os.path import join - -import platform - -Import("env") - - -# ===== -def _get_tool_path() -> str: - path = env.PioPlatform().get_package_dir("tool-avrdude") - assert exists(path) - return path - - -def _fix_ld_arm() -> None: - tool_path = _get_tool_path() - flag_path = join(tool_path, ".fix-ld-arm.done") - - if not exists(flag_path): - def patch(*_, **__) -> None: - symlink("/usr/lib/libtinfo.so.6", join(tool_path, "libtinfo.so.5")) - open(flag_path, "w").close() - - env.Execute(patch) - - -def _replace_to_system(new_path: str) -> None: - tool_path = _get_tool_path() - flag_path = join(tool_path, ".replace-to-system.done") - - if not exists(flag_path): - def patch(*_, **__) -> None: - old_path = join(tool_path, "avrdude") - bak_path = join(tool_path, "_avrdude_bak") - rename(old_path, bak_path) - symlink(new_path, old_path) - open(flag_path, "w").close() - - env.Execute(patch) - - -# ===== -if "arm" in platform.machine(): - _fix_ld_arm() - -_path = "/usr/bin/avrdude" -if exists(_path): - _replace_to_system(_path) diff --git a/hid/arduino/lib/drivers-avr/eeprom.h b/hid/arduino/lib/drivers-avr/eeprom.h deleted file mode 100644 index 9e727704..00000000 --- a/hid/arduino/lib/drivers-avr/eeprom.h +++ /dev/null @@ -1,40 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include - -#include "storage.h" - - -namespace DRIVERS { - struct Eeprom : public Storage { - using Storage::Storage; - - void readBlock(void *dest, const void *src, size_t size) override { - eeprom_read_block(dest, src, size); - } - - void updateBlock(const void *src, void *dest, size_t size) override { - eeprom_update_block(src, dest, size); - } - }; -} diff --git a/hid/arduino/lib/drivers-avr/factory.cpp b/hid/arduino/lib/drivers-avr/factory.cpp deleted file mode 100644 index 2fb54187..00000000 --- a/hid/arduino/lib/drivers-avr/factory.cpp +++ /dev/null @@ -1,94 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include "usb/hid.h" -#include "ps2/hid.h" -#include "factory.h" -#include "eeprom.h" -#include "serial.h" -#include "spi.h" - -#ifndef ARDUINO_ARCH_AVR -# error "Only AVR is supported" -#endif - - -namespace DRIVERS { - Keyboard *Factory::makeKeyboard(type _type) { - switch (_type) { -# ifdef HID_WITH_USB - case USB_KEYBOARD: - return new UsbKeyboard(); -# endif - -# ifdef HID_WITH_PS2 - case PS2_KEYBOARD: - return new Ps2Keyboard(); -# endif - - default: - return new Keyboard(DUMMY); - } - } - - Mouse *Factory::makeMouse(type _type) { - switch (_type) { -# ifdef HID_WITH_USB - case USB_MOUSE_ABSOLUTE: - case USB_MOUSE_ABSOLUTE_WIN98: - return new UsbMouseAbsolute(_type); - case USB_MOUSE_RELATIVE: - return new UsbMouseRelative(); -# endif - default: - return new Mouse(DRIVERS::DUMMY); - } - } - - Storage *Factory::makeStorage(type _type) { - switch (_type) { -# ifdef HID_DYNAMIC - case NON_VOLATILE_STORAGE: - return new Eeprom(DRIVERS::NON_VOLATILE_STORAGE); -# endif - default: - return new Storage(DRIVERS::DUMMY); - } - } - - Board *Factory::makeBoard(type _type) { - switch (_type) { - default: - return new Board(DRIVERS::DUMMY); - } - } - - Connection *Factory::makeConnection(type _type) { -# ifdef CMD_SERIAL - return new Serial(); -# elif defined(CMD_SPI) - return new Spi(); -# else -# error CMD phy is not defined -# endif - } -} diff --git a/hid/arduino/lib/drivers-avr/ps2/hid.h b/hid/arduino/lib/drivers-avr/ps2/hid.h deleted file mode 100644 index 56cc321d..00000000 --- a/hid/arduino/lib/drivers-avr/ps2/hid.h +++ /dev/null @@ -1,95 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include -#include - -#include "keyboard.h" -#include "keymap.h" - -// #define HID_PS2_KBD_CLOCK_PIN 7 -// #define HID_PS2_KBD_DATA_PIN 5 - - -class Ps2Keyboard : public DRIVERS::Keyboard { - // https://wiki.osdev.org/PS/2_Keyboard - - public: - Ps2Keyboard() : DRIVERS::Keyboard(DRIVERS::PS2_KEYBOARD), _dev(HID_PS2_KBD_CLOCK_PIN, HID_PS2_KBD_DATA_PIN) {} - - void begin() override { - _dev.keyboard_init(); - } - - void periodic() override { - _dev.keyboard_handle(&_leds); - } - - void sendKey(uint8_t code, bool state) override { - Ps2KeyType ps2_type; - uint8_t ps2_code; - - keymapPs2(code, &ps2_type, &ps2_code); - if (ps2_type != PS2_KEY_TYPE_UNKNOWN) { - // Не отправлялась часть нажатий. Когда clock на нуле, комп не принимает ничего от клавы. - // Этот костыль понижает процент пропущенных нажатий. - while (digitalRead(HID_PS2_KBD_CLOCK_PIN) == 0) {}; - if (state) { - switch (ps2_type) { - case PS2_KEY_TYPE_REG: _dev.keyboard_press(ps2_code); break; - case PS2_KEY_TYPE_SPEC: _dev.keyboard_press_special(ps2_code); break; - case PS2_KEY_TYPE_PRINT: _dev.keyboard_press_printscreen(); break; - case PS2_KEY_TYPE_PAUSE: _dev.keyboard_pausebreak(); break; - case PS2_KEY_TYPE_UNKNOWN: break; - } - } else { - switch (ps2_type) { - case PS2_KEY_TYPE_REG: _dev.keyboard_release(ps2_code); break; - case PS2_KEY_TYPE_SPEC: _dev.keyboard_release_special(ps2_code); break; - case PS2_KEY_TYPE_PRINT: _dev.keyboard_release_printscreen(); break; - case PS2_KEY_TYPE_PAUSE: break; - case PS2_KEY_TYPE_UNKNOWN: break; - } - } - } - } - - bool isOffline() override { - return false; - } - - KeyboardLedsState getLeds() override { - periodic(); - KeyboardLedsState result = { - .caps = _leds & 0b00000100, - .scroll = _leds & 0b00000001, - .num = _leds & 0b00000010, - }; - return result; - } - - private: - PS2dev _dev; - uint8_t _leds = 0; -}; diff --git a/hid/arduino/lib/drivers-avr/ps2/keymap.h b/hid/arduino/lib/drivers-avr/ps2/keymap.h deleted file mode 100644 index b864c8c1..00000000 --- a/hid/arduino/lib/drivers-avr/ps2/keymap.h +++ /dev/null @@ -1,152 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - - -enum Ps2KeyType : uint8_t { - PS2_KEY_TYPE_UNKNOWN = 0, - PS2_KEY_TYPE_REG = 1, - PS2_KEY_TYPE_SPEC = 2, - PS2_KEY_TYPE_PRINT = 3, - PS2_KEY_TYPE_PAUSE = 4, -}; - - -void keymapPs2(uint8_t code, Ps2KeyType *ps2_type, uint8_t *ps2_code) { - *ps2_type = PS2_KEY_TYPE_UNKNOWN; - *ps2_code = 0; - - switch (code) { - case 1: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 28; return; // KeyA - case 2: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 50; return; // KeyB - case 3: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 33; return; // KeyC - case 4: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 35; return; // KeyD - case 5: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 36; return; // KeyE - case 6: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 43; return; // KeyF - case 7: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 52; return; // KeyG - case 8: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 51; return; // KeyH - case 9: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 67; return; // KeyI - case 10: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 59; return; // KeyJ - case 11: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 66; return; // KeyK - case 12: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 75; return; // KeyL - case 13: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 58; return; // KeyM - case 14: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 49; return; // KeyN - case 15: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 68; return; // KeyO - case 16: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 77; return; // KeyP - case 17: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 21; return; // KeyQ - case 18: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 45; return; // KeyR - case 19: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 27; return; // KeyS - case 20: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 44; return; // KeyT - case 21: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 60; return; // KeyU - case 22: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 42; return; // KeyV - case 23: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 29; return; // KeyW - case 24: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 34; return; // KeyX - case 25: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 53; return; // KeyY - case 26: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 26; return; // KeyZ - case 27: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 22; return; // Digit1 - case 28: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 30; return; // Digit2 - case 29: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 38; return; // Digit3 - case 30: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 37; return; // Digit4 - case 31: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 46; return; // Digit5 - case 32: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 54; return; // Digit6 - case 33: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 61; return; // Digit7 - case 34: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 62; return; // Digit8 - case 35: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 70; return; // Digit9 - case 36: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 69; return; // Digit0 - case 37: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 90; return; // Enter - case 38: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 118; return; // Escape - case 39: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 102; return; // Backspace - case 40: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 13; return; // Tab - case 41: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 41; return; // Space - case 42: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 78; return; // Minus - case 43: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 85; return; // Equal - case 44: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 84; return; // BracketLeft - case 45: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 91; return; // BracketRight - case 46: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 93; return; // Backslash - case 47: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 76; return; // Semicolon - case 48: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 82; return; // Quote - case 49: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 14; return; // Backquote - case 50: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 65; return; // Comma - case 51: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 73; return; // Period - case 52: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 74; return; // Slash - case 53: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 88; return; // CapsLock - case 54: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 5; return; // F1 - case 55: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 6; return; // F2 - case 56: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 4; return; // F3 - case 57: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 12; return; // F4 - case 58: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 3; return; // F5 - case 59: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 11; return; // F6 - case 60: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 131; return; // F7 - case 61: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 10; return; // F8 - case 62: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 1; return; // F9 - case 63: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 9; return; // F10 - case 64: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 120; return; // F11 - case 65: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 7; return; // F12 - case 66: *ps2_type = PS2_KEY_TYPE_PRINT; *ps2_code = 255; return; // PrintScreen - case 67: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 112; return; // Insert - case 68: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 108; return; // Home - case 69: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 125; return; // PageUp - case 70: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 113; return; // Delete - case 71: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 105; return; // End - case 72: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 122; return; // PageDown - case 73: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 116; return; // ArrowRight - case 74: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 107; return; // ArrowLeft - case 75: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 114; return; // ArrowDown - case 76: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 117; return; // ArrowUp - case 77: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 20; return; // ControlLeft - case 78: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 18; return; // ShiftLeft - case 79: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 17; return; // AltLeft - case 80: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 31; return; // MetaLeft - case 81: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 20; return; // ControlRight - case 82: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 89; return; // ShiftRight - case 83: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 17; return; // AltRight - case 84: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 39; return; // MetaRight - case 85: *ps2_type = PS2_KEY_TYPE_PAUSE; *ps2_code = 255; return; // Pause - case 86: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 126; return; // ScrollLock - case 87: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 119; return; // NumLock - case 88: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 47; return; // ContextMenu - case 89: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 74; return; // NumpadDivide - case 90: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 124; return; // NumpadMultiply - case 91: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 123; return; // NumpadSubtract - case 92: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 121; return; // NumpadAdd - case 93: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 90; return; // NumpadEnter - case 94: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 105; return; // Numpad1 - case 95: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 114; return; // Numpad2 - case 96: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 122; return; // Numpad3 - case 97: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 107; return; // Numpad4 - case 98: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 115; return; // Numpad5 - case 99: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 116; return; // Numpad6 - case 100: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 108; return; // Numpad7 - case 101: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 117; return; // Numpad8 - case 102: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 125; return; // Numpad9 - case 103: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 112; return; // Numpad0 - case 104: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 113; return; // NumpadDecimal - case 105: *ps2_type = PS2_KEY_TYPE_SPEC; *ps2_code = 94; return; // Power - case 106: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 97; return; // IntlBackslash - case 107: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 106; return; // IntlYen - case 108: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 81; return; // IntlRo - case 109: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 19; return; // KanaMode - case 110: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 100; return; // Convert - case 111: *ps2_type = PS2_KEY_TYPE_REG; *ps2_code = 103; return; // NonConvert - } -} diff --git a/hid/arduino/lib/drivers-avr/ps2/keymap.h.mako b/hid/arduino/lib/drivers-avr/ps2/keymap.h.mako deleted file mode 100644 index 8e31e44d..00000000 --- a/hid/arduino/lib/drivers-avr/ps2/keymap.h.mako +++ /dev/null @@ -1,44 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - - -enum Ps2KeyType : uint8_t { - PS2_KEY_TYPE_UNKNOWN = 0, - PS2_KEY_TYPE_REG = 1, - PS2_KEY_TYPE_SPEC = 2, - PS2_KEY_TYPE_PRINT = 3, - PS2_KEY_TYPE_PAUSE = 4, -}; - -<%! import operator %> -void keymapPs2(uint8_t code, Ps2KeyType *ps2_type, uint8_t *ps2_code) { - *ps2_type = PS2_KEY_TYPE_UNKNOWN; - *ps2_code = 0; - - switch (code) { -% for km in sorted(keymap, key=operator.attrgetter("mcu_code")): - case ${km.mcu_code}: *ps2_type = PS2_KEY_TYPE_${km.ps2_key.type.upper()}; *ps2_code = ${km.ps2_key.code}; return; // ${km.web_name} -% endfor - } -} diff --git a/hid/arduino/lib/drivers-avr/spi.cpp b/hid/arduino/lib/drivers-avr/spi.cpp deleted file mode 100644 index 9572c513..00000000 --- a/hid/arduino/lib/drivers-avr/spi.cpp +++ /dev/null @@ -1,83 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include "spi.h" - -#ifdef CMD_SPI - - -static volatile uint8_t _spi_in[8] = {0}; -static volatile uint8_t _spi_in_index = 0; - -static volatile uint8_t _spi_out[8] = {0}; -static volatile uint8_t _spi_out_index = 0; - - -namespace DRIVERS { - void Spi::begin() { - pinMode(MISO, OUTPUT); - SPCR = (1 << SPE) | (1 << SPIE); // Slave, SPI En, IRQ En - } - - void Spi::periodic() { - if (!_spi_out[0] && _spi_in_index == 8) { - _data_cb((const uint8_t *)_spi_in, 8); - } - } - - void Spi::write(const uint8_t *data, size_t size) { - // Меджик в нулевом байте разрешает начать ответ - for (int index = 7; index >= 0; --index) { - _spi_out[index] = data[index]; - } - } -} - -ISR(SPI_STC_vect) { - uint8_t in = SPDR; - if (_spi_out[0] && _spi_out_index < 8) { - SPDR = _spi_out[_spi_out_index]; - if (!(SPSR & (1 << WCOL))) { - ++_spi_out_index; - if (_spi_out_index == 8) { - _spi_out_index = 0; - _spi_in_index = 0; - _spi_out[0] = 0; - } - } - } else { - static bool receiving = false; - if (!receiving && in != 0) { - receiving = true; - } - if (receiving && _spi_in_index < 8) { - _spi_in[_spi_in_index] = in; - ++_spi_in_index; - } - if (_spi_in_index == 8) { - receiving = false; - } - SPDR = 0; - } -} - -#endif diff --git a/hid/arduino/lib/drivers-avr/spi.h b/hid/arduino/lib/drivers-avr/spi.h deleted file mode 100644 index 742a808d..00000000 --- a/hid/arduino/lib/drivers-avr/spi.h +++ /dev/null @@ -1,40 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include - -#include "connection.h" - - -namespace DRIVERS { - struct Spi : public Connection { - Spi() : Connection(CONNECTION) {} - - void begin() override; - - void periodic() override; - - void write(const uint8_t *data, size_t size) override; - }; -} diff --git a/hid/arduino/lib/drivers-avr/usb/hid.h b/hid/arduino/lib/drivers-avr/usb/hid.h deleted file mode 100644 index ce15f427..00000000 --- a/hid/arduino/lib/drivers-avr/usb/hid.h +++ /dev/null @@ -1,230 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include -#include - -#include "keyboard.h" -#include "mouse.h" -#include "tools.h" -#include "usb-keymap.h" -#ifdef AUM -# include "aum.h" -#endif - -using namespace DRIVERS; - -// ----------------------------------------------------------------------------- -#ifdef HID_USB_CHECK_ENDPOINT -// https://github.com/arduino/ArduinoCore-avr/blob/2f67c916f6ab6193c404eebe22efe901e0f9542d/cores/arduino/USBCore.cpp#L249 -// https://sourceforge.net/p/arduinomidilib/svn/41/tree/branch/3.1/Teensy/teensy_core/usb_midi/usb_api.cpp#l103 -# ifdef AUM -# define CHECK_AUM_USB { if (!aumIsUsbConnected()) { return true; } } -# else -# define CHECK_AUM_USB -# endif -# define CLS_IS_OFFLINE(_hid) \ - bool isOffline() override { \ - CHECK_AUM_USB; \ - uint8_t ep = _hid.getPluggedEndpoint(); \ - uint8_t intr_state = SREG; \ - cli(); \ - UENUM = ep & 7; \ - bool rw_allowed = UEINTX & (1 << RWAL); \ - SREG = intr_state; \ - if (rw_allowed) { \ - return false; \ - } \ - return true; \ - } -# define CHECK_HID_EP { if (isOffline()) return; } - -#else -# define CLS_IS_OFFLINE(_hid) \ - bool isOffline() override { \ - return false; \ - } -# define CHECK_HID_EP - -#endif - - -class UsbKeyboard : public DRIVERS::Keyboard { - public: - UsbKeyboard() : DRIVERS::Keyboard(DRIVERS::USB_KEYBOARD) {} - - void begin() override { - _kbd.begin(); - } - - void periodic() override { -# ifdef HID_USB_CHECK_ENDPOINT - static unsigned long prev_ts = 0; - if (is_micros_timed_out(prev_ts, 50000)) { - static bool prev_online = true; - bool online = !isOffline(); - if (!_sent || (online && !prev_online)) { - _sendCurrent(); - } - prev_online = online; - prev_ts = micros(); - } -# endif - } - - void clear() override { - _kbd.releaseAll(); - } - - void sendKey(uint8_t code, bool state) override { - enum KeyboardKeycode usb_code = keymapUsb(code); - if (usb_code > 0) { - if (state ? _kbd.add(usb_code) : _kbd.remove(usb_code)) { - _sendCurrent(); - } - } - } - - CLS_IS_OFFLINE(_kbd) - - KeyboardLedsState getLeds() override { - uint8_t leds = _kbd.getLeds(); - KeyboardLedsState result = { - .caps = leds & LED_CAPS_LOCK, - .scroll = leds & LED_SCROLL_LOCK, - .num = leds & LED_NUM_LOCK, - }; - return result; - } - - private: - BootKeyboard_ _kbd; - bool _sent = true; - - void _sendCurrent() { -# ifdef HID_USB_CHECK_ENDPOINT - if (isOffline()) { - _sent = false; - } else { -# endif - _sent = (_kbd.send() >= 0); -# ifdef HID_USB_CHECK_ENDPOINT - } -# endif - } -}; - -#define CLS_SEND_BUTTONS \ - void sendButtons( \ - bool left_select, bool left_state, \ - bool right_select, bool right_state, \ - bool middle_select, bool middle_state, \ - bool up_select, bool up_state, \ - bool down_select, bool down_state \ - ) override { \ - if (left_select) _sendButton(MOUSE_LEFT, left_state); \ - if (right_select) _sendButton(MOUSE_RIGHT, right_state); \ - if (middle_select) _sendButton(MOUSE_MIDDLE, middle_state); \ - if (up_select) _sendButton(MOUSE_PREV, up_state); \ - if (down_select) _sendButton(MOUSE_NEXT, down_state); \ - } - -class UsbMouseAbsolute : public DRIVERS::Mouse { - public: - UsbMouseAbsolute(DRIVERS::type _type) : Mouse(_type) {} - - void begin() override { - _mouse.begin(); - _mouse.setWin98FixEnabled(getType() == DRIVERS::USB_MOUSE_ABSOLUTE_WIN98); - } - - void clear() override { - _mouse.releaseAll(); - } - - CLS_SEND_BUTTONS - - void sendMove(int x, int y) override { - CHECK_HID_EP; - _mouse.moveTo(x, y); - } - - void sendWheel(int delta_y) override { - // delta_x is not supported by hid-project now - CHECK_HID_EP; - _mouse.move(0, 0, delta_y); - } - - CLS_IS_OFFLINE(_mouse) - - private: - SingleAbsoluteMouse_ _mouse; - - void _sendButton(uint8_t button, bool state) { - CHECK_HID_EP; - if (state) _mouse.press(button); - else _mouse.release(button); - } -}; - -class UsbMouseRelative : public DRIVERS::Mouse { - public: - UsbMouseRelative() : DRIVERS::Mouse(DRIVERS::USB_MOUSE_RELATIVE) {} - - void begin() override { - _mouse.begin(); - } - - void clear() override { - _mouse.releaseAll(); - } - - CLS_SEND_BUTTONS - - void sendRelative(int x, int y) override { - CHECK_HID_EP; - _mouse.move(x, y, 0); - } - - void sendWheel(int delta_y) override { - // delta_x is not supported by hid-project now - CHECK_HID_EP; - _mouse.move(0, 0, delta_y); - } - - CLS_IS_OFFLINE(_mouse) - - private: - BootMouse_ _mouse; - - void _sendButton(uint8_t button, bool state) { - CHECK_HID_EP; - if (state) _mouse.press(button); - else _mouse.release(button); - } -}; - -#undef CLS_SEND_BUTTONS -#undef CLS_IS_OFFLINE -#undef CHECK_HID_EP diff --git a/hid/arduino/lib/drivers-stm32/README.md b/hid/arduino/lib/drivers-stm32/README.md deleted file mode 100644 index 93de3312..00000000 --- a/hid/arduino/lib/drivers-stm32/README.md +++ /dev/null @@ -1,22 +0,0 @@ -This is WIP. Use AVR version as reference. - -It was tested with bluepill. Most boards are clones. -If you have problem with USB please check https://stm32duinoforum.com/forum/wiki_subdomain/index_title_Blue_Pill.html for pull up resistor. If it still does not work check another board or cable. - -TODO: -- [x] Serial communication -- [x] USB keyboard -- [x] USB keyboard - add scroll status -- [x] USB keyboard - key sending -- [x] USB keyboard - test key mapping -- [x] Persistent storage -- [ ] SPI communication -- [ ] PS2 keyboard -- [x] USB absolute mouse -- [x] USB absolute mouse - add whele -- [x] USB relative mouse -- [x] USB relative mouse - add whele -- [ ] USB mouses - up down button -- [ ] WIN98 USB mouse -- [x] undefine SERIAL_USB -- [ ] boot keyboard diff --git a/hid/arduino/lib/drivers-stm32/backup-register.h b/hid/arduino/lib/drivers-stm32/backup-register.h deleted file mode 100644 index 6265dd04..00000000 --- a/hid/arduino/lib/drivers-stm32/backup-register.h +++ /dev/null @@ -1,53 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - -# pragma once - - -#include - -#include "storage.h" - - -namespace DRIVERS { - struct BackupRegister : public Storage { - BackupRegister() : Storage(NON_VOLATILE_STORAGE) { - _rtc.enableClockInterface(); - } - - void readBlock(void *dest, const void *src, size_t size) override { - uint8_t *dest_ = reinterpret_cast(dest); - for(size_t index = 0; index < size; ++index) { - dest_[index] = _rtc.getBackupRegister(reinterpret_cast(src) + index + 1); - } - } - - void updateBlock(const void *src, void *dest, size_t size) override { - const uint8_t *src_ = reinterpret_cast(src); - for(size_t index = 0; index < size; ++index) { - _rtc.setBackupRegister(reinterpret_cast(dest) + index + 1, src_[index]); - } - } - - private: - STM32F1_RTC _rtc; - }; -} diff --git a/hid/arduino/lib/drivers-stm32/bluepill_sch.png b/hid/arduino/lib/drivers-stm32/bluepill_sch.png deleted file mode 100644 index 956448ddd28d3fe45d756dde0db459618d28e607..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 179860 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 LoK&LZb@%@TJm~xi diff --git a/hid/arduino/lib/drivers-stm32/board-stm32.h b/hid/arduino/lib/drivers-stm32/board-stm32.h deleted file mode 100644 index 97411cb2..00000000 --- a/hid/arduino/lib/drivers-stm32/board-stm32.h +++ /dev/null @@ -1,103 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "board.h" -#include - - -namespace DRIVERS { - class BoardStm32 : public Board { - public: - BoardStm32() : Board(BOARD){ - //2 sec timeout - iwdg_init(IWDG_PRE_16, 0xFFF); - pinMode(LED_BUILTIN, OUTPUT); - } - - void reset() override { - nvic_sys_reset(); - } - - void periodic() override { - iwdg_feed(); - if (is_micros_timed_out(_prev_ts, 100000)) { - switch(_state) { - case 0: - digitalWrite(LED_BUILTIN, LOW); - break; - case 2: - if(_rx_data) { - _rx_data = false; - digitalWrite(LED_BUILTIN, LOW); - } - break; - case 4: - if(_keyboard_online) { - _keyboard_online = false; - digitalWrite(LED_BUILTIN, LOW); - } - break; - case 8: - if(_mouse_online) { - _mouse_online = false; - digitalWrite(LED_BUILTIN, LOW); - } - break; - case 1: // heartbeat off - case 3: // _rx_data off - case 7: // _keyboard_online off - case 11: // _mouse_online off - digitalWrite(LED_BUILTIN, HIGH); - break; - case 19: - _state = -1; - break; - } - ++_state; - _prev_ts = micros(); - } - } - - void updateStatus(status status) override { - switch (status) { - case RX_DATA: - _rx_data = true; - break; - case KEYBOARD_ONLINE: - _keyboard_online = true; - break; - case MOUSE_ONLINE: - _mouse_online = true; - break; - } - } - - private: - unsigned long _prev_ts = 0; - uint8_t _state = 0; - bool _rx_data = false; - bool _keyboard_online = false; - bool _mouse_online = false; - }; -} diff --git a/hid/arduino/lib/drivers-stm32/factory.cpp b/hid/arduino/lib/drivers-stm32/factory.cpp deleted file mode 100644 index 9540bb7d..00000000 --- a/hid/arduino/lib/drivers-stm32/factory.cpp +++ /dev/null @@ -1,94 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include "factory.h" -#include "usb/keyboard-stm32.h" -#include "usb/hid-wrapper-stm32.h" -#include "usb/mouse-absolute-stm32.h" -#include "usb/mouse-relative-stm32.h" -#include "backup-register.h" -#include "board-stm32.h" -#include "serial.h" - -#ifndef __STM32F1__ -# error "Only STM32F1 is supported" -#endif -#ifdef SERIAL_USB -# error "Disable random USB enumeration" -#endif - - -namespace DRIVERS { - HidWrapper _hidWrapper; - - Keyboard *Factory::makeKeyboard(type _type) { - switch (_type) { -# ifdef HID_WITH_USB - case USB_KEYBOARD: - return new UsbKeyboard(_hidWrapper); -# endif - default: - return new Keyboard(DUMMY); - } - } - - Mouse *Factory::makeMouse(type _type) { - switch(_type) { -# ifdef HID_WITH_USB - case USB_MOUSE_ABSOLUTE: - return new UsbMouseAbsolute(_hidWrapper); - case USB_MOUSE_RELATIVE: - return new UsbMouseRelative(_hidWrapper); -# endif - default: - return new Mouse(DRIVERS::DUMMY); - } - } - - Storage *Factory::makeStorage(type _type) { - switch (_type) { -# ifdef HID_DYNAMIC - case NON_VOLATILE_STORAGE: - return new BackupRegister(); -# endif - default: - return new Storage(DRIVERS::DUMMY); - } - } - - Board *Factory::makeBoard(type _type) { - switch (_type) { - case BOARD: - return new BoardStm32(); - default: - return new Board(DRIVERS::DUMMY); - } - } - - Connection *Factory::makeConnection(type _type) { -# ifdef CMD_SERIAL - return new Serial(); -# else -# error CMD phy is not defined -# endif - } -} diff --git a/hid/arduino/lib/drivers-stm32/usb/hid-wrapper-stm32.h b/hid/arduino/lib/drivers-stm32/usb/hid-wrapper-stm32.h deleted file mode 100644 index 3df0f4ea..00000000 --- a/hid/arduino/lib/drivers-stm32/usb/hid-wrapper-stm32.h +++ /dev/null @@ -1,72 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include - - -namespace DRIVERS { - class HidWrapper { - public: - void begin() { - if (_init) { - return; - } - _init = true; - - _report_descriptor_length = 0; - for (unsigned index = 0; index < _count; ++index) { - _report_descriptor_length += _descriptors_size[index]; - } - - _report_descriptor = new uint8[_report_descriptor_length]; - - size_t offset = 0; - for (unsigned index = 0; index < _count; ++index) { - memcpy(_report_descriptor + offset, _report_descriptors[index], _descriptors_size[index]); - offset += _descriptors_size[index]; - } - - usbHid.begin(_report_descriptor, _report_descriptor_length); - } - - void addReportDescriptor(const uint8_t *report_descriptor, uint16_t report_descriptor_length) { - _report_descriptors[_count] = report_descriptor; - _descriptors_size[_count] = report_descriptor_length; - ++_count; - } - - USBHID usbHid; - - private: - bool _init = false; - - static constexpr uint8_t MAX_USB_DESCRIPTORS = 2; - const uint8_t *_report_descriptors[MAX_USB_DESCRIPTORS]; - uint8_t _descriptors_size[MAX_USB_DESCRIPTORS]; - - uint8_t _count = 0; - uint8_t *_report_descriptor; - uint16_t _report_descriptor_length; - }; -} diff --git a/hid/arduino/lib/drivers-stm32/usb/keyboard-stm32.h b/hid/arduino/lib/drivers-stm32/usb/keyboard-stm32.h deleted file mode 100644 index 578e0751..00000000 --- a/hid/arduino/lib/drivers-stm32/usb/keyboard-stm32.h +++ /dev/null @@ -1,92 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include - -#include "tools.h" -#include "keyboard.h" -#include "usb-keymap.h" -#include "hid-wrapper-stm32.h" - - -namespace DRIVERS { - const uint8_t reportDescriptionKeyboard[] = { - HID_KEYBOARD_REPORT_DESCRIPTOR(), - }; - - class UsbKeyboard : public Keyboard { - public: - UsbKeyboard(HidWrapper& _hidWrapper) : Keyboard(USB_KEYBOARD), - _hidWrapper(_hidWrapper), _keyboard(_hidWrapper.usbHid) { - _hidWrapper.addReportDescriptor(reportDescriptionKeyboard, sizeof(reportDescriptionKeyboard)); - } - - void begin() override { - _hidWrapper.begin(); - _keyboard.begin(); - } - - void clear() override { - _keyboard.releaseAll(); - } - - void sendKey(uint8_t code, bool state) override { - uint16_t usb_code = keymapUsb(code); - if (usb_code == 0) { - return; - } - - // 0xE0 is a prefix from HID-Project keytable - if (usb_code >= 0xE0 && usb_code <= 0xE7) { - usb_code = usb_code - 0xE0 + 0x80; - } else { - usb_code += KEY_HID_OFFSET; - } - - if (state) { - _keyboard.press(usb_code); - } else { - _keyboard.release(usb_code); - } - } - - bool isOffline() override { - return (USBComposite == false); - } - - KeyboardLedsState getLeds() override { - uint8_t leds = _keyboard.getLEDs(); - KeyboardLedsState result = { - .caps = leds & 0b00000010, - .scroll = leds & 0b00000100, - .num = leds & 0b00000001, - }; - return result; - } - - private: - HidWrapper& _hidWrapper; - HIDKeyboard _keyboard; - }; -} diff --git a/hid/arduino/lib/drivers-stm32/usb/mouse-absolute-stm32.h b/hid/arduino/lib/drivers-stm32/usb/mouse-absolute-stm32.h deleted file mode 100644 index 4090cd4f..00000000 --- a/hid/arduino/lib/drivers-stm32/usb/mouse-absolute-stm32.h +++ /dev/null @@ -1,86 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include - -#include "mouse.h" -#include "hid-wrapper-stm32.h" - - -namespace DRIVERS { - const uint8_t reportDescriptionMouseAbsolute[] = { - HID_ABS_MOUSE_REPORT_DESCRIPTOR() - }; - - class UsbMouseAbsolute : public Mouse { - public: - UsbMouseAbsolute(HidWrapper& _hidWrapper) : Mouse(USB_MOUSE_ABSOLUTE), - _hidWrapper(_hidWrapper), _mouse(_hidWrapper.usbHid) { - _hidWrapper.addReportDescriptor(reportDescriptionMouseAbsolute, sizeof(reportDescriptionMouseAbsolute)); - } - - void begin() override { - _hidWrapper.begin(); - } - - void clear() override { - _mouse.release(0xFF); - } - - void sendButtons ( - bool left_select, bool left_state, - bool right_select, bool right_state, - bool middle_select, bool middle_state, - bool up_select, bool up_state, - bool down_select, bool down_state) override { - -# define SEND_BUTTON(x_low, x_up) { \ - if (x_low##_select) { \ - if (x_low##_state) _mouse.press(MOUSE_##x_up); \ - else _mouse.release(MOUSE_##x_up); \ - } \ - } - SEND_BUTTON(left, LEFT); - SEND_BUTTON(right, RIGHT); - SEND_BUTTON(middle, MIDDLE); -# undef SEND_BUTTON - } - - void sendMove(int x, int y) override { - _mouse.move(x, y); - } - - void sendWheel(int delta_y) override { - _mouse.move(0, 0, delta_y); - } - - bool isOffline() override { - return (USBComposite == false); - } - - private: - HidWrapper& _hidWrapper; - HIDAbsMouse _mouse; - }; -} diff --git a/hid/arduino/lib/drivers-stm32/usb/mouse-relative-stm32.h b/hid/arduino/lib/drivers-stm32/usb/mouse-relative-stm32.h deleted file mode 100644 index 943857ef..00000000 --- a/hid/arduino/lib/drivers-stm32/usb/mouse-relative-stm32.h +++ /dev/null @@ -1,86 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include - -#include "mouse.h" -#include "hid-wrapper-stm32.h" - - -namespace DRIVERS { - const uint8_t reportDescriptionMouseRelative[] = { - HID_MOUSE_REPORT_DESCRIPTOR() - }; - - class UsbMouseRelative : public Mouse { - public: - UsbMouseRelative(HidWrapper& _hidWrapper) : Mouse(USB_MOUSE_RELATIVE), - _hidWrapper(_hidWrapper), _mouse(_hidWrapper.usbHid) { - _hidWrapper.addReportDescriptor(reportDescriptionMouseRelative, sizeof(reportDescriptionMouseRelative)); - } - - void begin() override { - _hidWrapper.begin(); - } - - void clear() override { - _mouse.release(0xFF); - } - - void sendButtons ( - bool left_select, bool left_state, - bool right_select, bool right_state, - bool middle_select, bool middle_state, - bool up_select, bool up_state, - bool down_select, bool down_state) override { - -# define SEND_BUTTON(x_low, x_up) { \ - if (x_low##_select) { \ - if (x_low##_state) _mouse.press(MOUSE_##x_up); \ - else _mouse.release(MOUSE_##x_up); \ - } \ - } - SEND_BUTTON(left, LEFT); - SEND_BUTTON(right, RIGHT); - SEND_BUTTON(middle, MIDDLE); -# undef SEND_BUTTON - } - - void sendRelative(int x, int y) override { - _mouse.move(x, y); - } - - void sendWheel(int delta_y) override { - _mouse.move(0, 0, delta_y); - } - - bool isOffline() override { - return (USBComposite == false); - } - - private: - HidWrapper& _hidWrapper; - HIDMouse _mouse; - }; -} diff --git a/hid/arduino/lib/drivers/aum.h b/hid/arduino/lib/drivers/aum.h deleted file mode 100644 index b211304a..00000000 --- a/hid/arduino/lib/drivers/aum.h +++ /dev/null @@ -1,48 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include - - -inline void aumInit() { - pinModeFast(AUM_IS_USB_POWERED_PIN, INPUT); - pinModeFast(AUM_SET_USB_VBUS_PIN, OUTPUT); - pinModeFast(AUM_SET_USB_CONNECTED_PIN, OUTPUT); - digitalWriteFast(AUM_SET_USB_CONNECTED_PIN, HIGH); -} - -inline void aumProxyUsbVbus() { - bool vbus = digitalReadFast(AUM_IS_USB_POWERED_PIN); - if (digitalReadFast(AUM_SET_USB_VBUS_PIN) != vbus) { - digitalWriteFast(AUM_SET_USB_VBUS_PIN, vbus); - } -} - -inline void aumSetUsbConnected(bool connected) { - digitalWriteFast(AUM_SET_USB_CONNECTED_PIN, connected); -} - -inline bool aumIsUsbConnected() { - return digitalReadFast(AUM_SET_USB_CONNECTED_PIN); -} diff --git a/hid/arduino/lib/drivers/board.h b/hid/arduino/lib/drivers/board.h deleted file mode 100644 index 069bbb0e..00000000 --- a/hid/arduino/lib/drivers/board.h +++ /dev/null @@ -1,41 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "driver.h" - - -namespace DRIVERS { - enum status { - RX_DATA = 0, - KEYBOARD_ONLINE, - MOUSE_ONLINE, - }; - - struct Board : public Driver { - using Driver::Driver; - virtual void reset() {} - virtual void periodic() {} - virtual void updateStatus(status status) {} - }; -} diff --git a/hid/arduino/lib/drivers/connection.h b/hid/arduino/lib/drivers/connection.h deleted file mode 100644 index f0e2ca0b..00000000 --- a/hid/arduino/lib/drivers/connection.h +++ /dev/null @@ -1,54 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "driver.h" -#include "stdint.h" - - -namespace DRIVERS { - typedef void (*DataHandler)(const uint8_t *data, size_t size); - typedef void (*TimeoutHandler)(); - - struct Connection : public Driver { - using Driver::Driver; - - virtual void begin() {} - - virtual void periodic() {} - - void onTimeout(TimeoutHandler cb) { - _timeout_cb = cb; - } - - void onData(DataHandler cb) { - _data_cb = cb; - } - - virtual void write(const uint8_t *data, size_t size) = 0; - - protected: - TimeoutHandler _timeout_cb = nullptr; - DataHandler _data_cb = nullptr; - }; -} diff --git a/hid/arduino/lib/drivers/driver.h b/hid/arduino/lib/drivers/driver.h deleted file mode 100644 index 7fcb72dd..00000000 --- a/hid/arduino/lib/drivers/driver.h +++ /dev/null @@ -1,49 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include - - -namespace DRIVERS { - enum type { - DUMMY = 0, - USB_MOUSE_ABSOLUTE, - USB_MOUSE_RELATIVE, - USB_MOUSE_ABSOLUTE_WIN98, - USB_KEYBOARD, - PS2_KEYBOARD, - NON_VOLATILE_STORAGE, - BOARD, - CONNECTION, - }; - - class Driver { - public: - Driver(type _type) : _type(_type) {} - uint8_t getType() { return _type; } - - private: - type _type; - }; -} diff --git a/hid/arduino/lib/drivers/factory.h b/hid/arduino/lib/drivers/factory.h deleted file mode 100644 index 5eeba394..00000000 --- a/hid/arduino/lib/drivers/factory.h +++ /dev/null @@ -1,39 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - -#pragma once - -#include "keyboard.h" -#include "mouse.h" -#include "storage.h" -#include "board.h" -#include "connection.h" - - -namespace DRIVERS { - struct Factory { - static Keyboard *makeKeyboard(type _type); - static Mouse *makeMouse(type _type); - static Storage *makeStorage(type _type); - static Board *makeBoard(type _type); - static Connection *makeConnection(type _type); - }; -} diff --git a/hid/arduino/lib/drivers/keyboard.h b/hid/arduino/lib/drivers/keyboard.h deleted file mode 100644 index b853ba67..00000000 --- a/hid/arduino/lib/drivers/keyboard.h +++ /dev/null @@ -1,68 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include - -#include "driver.h" - - -namespace DRIVERS { - typedef struct { - bool caps; - bool scroll; - bool num; - } KeyboardLedsState; - - struct Keyboard : public Driver { - using Driver::Driver; - - virtual void begin() {} - - /** - * Release all keys - */ - virtual void clear() {} - - /** - * Sends key - * @param code ??? - * @param state true pressed, false released - */ - virtual void sendKey(uint8_t code, bool state) {} - - virtual void periodic() {} - - /** - * False if online or unknown. Otherwise true. - */ - virtual bool isOffline() { - return false; - } - - virtual KeyboardLedsState getLeds() { - KeyboardLedsState result = {0}; - return result; - } - }; -} diff --git a/hid/arduino/lib/drivers/mouse.h b/hid/arduino/lib/drivers/mouse.h deleted file mode 100644 index 1877bf1e..00000000 --- a/hid/arduino/lib/drivers/mouse.h +++ /dev/null @@ -1,51 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include - -#include "driver.h" - - -namespace DRIVERS { - struct Mouse : public Driver { - using Driver::Driver; - virtual void begin() {} - - /** - * Release all keys - */ - virtual void clear() {} - virtual void sendButtons( - bool left_select, bool left_state, - bool right_select, bool right_state, - bool middle_select, bool middle_state, - bool up_select, bool up_state, - bool down_select, bool down_state) {} - virtual void sendMove(int x, int y) {} - virtual void sendRelative(int x, int y) {} - virtual void sendWheel(int delta_y) {} - virtual bool isOffline() { return false; } - virtual void periodic() {} - }; -} diff --git a/hid/arduino/lib/drivers/serial.h b/hid/arduino/lib/drivers/serial.h deleted file mode 100644 index 60ade223..00000000 --- a/hid/arduino/lib/drivers/serial.h +++ /dev/null @@ -1,68 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#ifdef CMD_SERIAL -#include "connection.h" - - -namespace DRIVERS { -#ifdef Serial -# undef Serial -#endif - struct Serial : public Connection { - Serial() : Connection(CONNECTION) {} - - void begin() override { - CMD_SERIAL.begin(CMD_SERIAL_SPEED); - } - - void periodic() override { - if (CMD_SERIAL.available() > 0) { - _buffer[_index] = (uint8_t)CMD_SERIAL.read(); - if (_index == 7) { - _data_cb(_buffer, 8); - _index = 0; - } else { - _last = micros(); - ++_index; - } - } else if (_index > 0) { - if (is_micros_timed_out(_last, CMD_SERIAL_TIMEOUT)) { - _timeout_cb(); - _index = 0; - } - } - } - - void write(const uint8_t *data, size_t size) override { - CMD_SERIAL.write(data, size); - } - - private: - unsigned long _last = 0; - uint8_t _index = 0; - uint8_t _buffer[8]; - }; -} -#endif diff --git a/hid/arduino/lib/drivers/storage.h b/hid/arduino/lib/drivers/storage.h deleted file mode 100644 index 19a5a770..00000000 --- a/hid/arduino/lib/drivers/storage.h +++ /dev/null @@ -1,35 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "driver.h" -#include "stdlib.h" - - -namespace DRIVERS { - struct Storage : public Driver { - using Driver::Driver; - virtual void readBlock(void *dest, const void *src, size_t size) {} - virtual void updateBlock(const void *src, void *dest, size_t size) {} - }; -} diff --git a/hid/arduino/lib/drivers/tools.cpp b/hid/arduino/lib/drivers/tools.cpp deleted file mode 100644 index 3dbd8470..00000000 --- a/hid/arduino/lib/drivers/tools.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include "tools.h" - - -bool is_micros_timed_out(unsigned long start_ts, unsigned long timeout) { - unsigned long now = micros(); - return ( - (now >= start_ts && now - start_ts > timeout) - || (now < start_ts && ((unsigned long)-1) - start_ts + now > timeout) - ); -} diff --git a/hid/arduino/lib/drivers/tools.h b/hid/arduino/lib/drivers/tools.h deleted file mode 100644 index ea75e378..00000000 --- a/hid/arduino/lib/drivers/tools.h +++ /dev/null @@ -1,28 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include - - -bool is_micros_timed_out(unsigned long start_ts, unsigned long timeout); diff --git a/hid/arduino/lib/drivers/usb-keymap.h b/hid/arduino/lib/drivers/usb-keymap.h deleted file mode 100644 index 95134453..00000000 --- a/hid/arduino/lib/drivers/usb-keymap.h +++ /dev/null @@ -1,141 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - - -uint8_t keymapUsb(uint8_t code) { - switch (code) { - case 1: return 4; // KeyA - case 2: return 5; // KeyB - case 3: return 6; // KeyC - case 4: return 7; // KeyD - case 5: return 8; // KeyE - case 6: return 9; // KeyF - case 7: return 10; // KeyG - case 8: return 11; // KeyH - case 9: return 12; // KeyI - case 10: return 13; // KeyJ - case 11: return 14; // KeyK - case 12: return 15; // KeyL - case 13: return 16; // KeyM - case 14: return 17; // KeyN - case 15: return 18; // KeyO - case 16: return 19; // KeyP - case 17: return 20; // KeyQ - case 18: return 21; // KeyR - case 19: return 22; // KeyS - case 20: return 23; // KeyT - case 21: return 24; // KeyU - case 22: return 25; // KeyV - case 23: return 26; // KeyW - case 24: return 27; // KeyX - case 25: return 28; // KeyY - case 26: return 29; // KeyZ - case 27: return 30; // Digit1 - case 28: return 31; // Digit2 - case 29: return 32; // Digit3 - case 30: return 33; // Digit4 - case 31: return 34; // Digit5 - case 32: return 35; // Digit6 - case 33: return 36; // Digit7 - case 34: return 37; // Digit8 - case 35: return 38; // Digit9 - case 36: return 39; // Digit0 - case 37: return 40; // Enter - case 38: return 41; // Escape - case 39: return 42; // Backspace - case 40: return 43; // Tab - case 41: return 44; // Space - case 42: return 45; // Minus - case 43: return 46; // Equal - case 44: return 47; // BracketLeft - case 45: return 48; // BracketRight - case 46: return 49; // Backslash - case 47: return 51; // Semicolon - case 48: return 52; // Quote - case 49: return 53; // Backquote - case 50: return 54; // Comma - case 51: return 55; // Period - case 52: return 56; // Slash - case 53: return 57; // CapsLock - case 54: return 58; // F1 - case 55: return 59; // F2 - case 56: return 60; // F3 - case 57: return 61; // F4 - case 58: return 62; // F5 - case 59: return 63; // F6 - case 60: return 64; // F7 - case 61: return 65; // F8 - case 62: return 66; // F9 - case 63: return 67; // F10 - case 64: return 68; // F11 - case 65: return 69; // F12 - case 66: return 70; // PrintScreen - case 67: return 73; // Insert - case 68: return 74; // Home - case 69: return 75; // PageUp - case 70: return 76; // Delete - case 71: return 77; // End - case 72: return 78; // PageDown - case 73: return 79; // ArrowRight - case 74: return 80; // ArrowLeft - case 75: return 81; // ArrowDown - case 76: return 82; // ArrowUp - case 77: return 224; // ControlLeft - case 78: return 225; // ShiftLeft - case 79: return 226; // AltLeft - case 80: return 227; // MetaLeft - case 81: return 228; // ControlRight - case 82: return 229; // ShiftRight - case 83: return 230; // AltRight - case 84: return 231; // MetaRight - case 85: return 72; // Pause - case 86: return 71; // ScrollLock - case 87: return 83; // NumLock - case 88: return 101; // ContextMenu - case 89: return 84; // NumpadDivide - case 90: return 85; // NumpadMultiply - case 91: return 86; // NumpadSubtract - case 92: return 87; // NumpadAdd - case 93: return 88; // NumpadEnter - case 94: return 89; // Numpad1 - case 95: return 90; // Numpad2 - case 96: return 91; // Numpad3 - case 97: return 92; // Numpad4 - case 98: return 93; // Numpad5 - case 99: return 94; // Numpad6 - case 100: return 95; // Numpad7 - case 101: return 96; // Numpad8 - case 102: return 97; // Numpad9 - case 103: return 98; // Numpad0 - case 104: return 99; // NumpadDecimal - case 105: return 102; // Power - case 106: return 100; // IntlBackslash - case 107: return 137; // IntlYen - case 108: return 135; // IntlRo - case 109: return 136; // KanaMode - case 110: return 138; // Convert - case 111: return 139; // NonConvert - default: return 0; - } -} diff --git a/hid/arduino/lib/drivers/usb-keymap.h.mako b/hid/arduino/lib/drivers/usb-keymap.h.mako deleted file mode 100644 index 5a10ac45..00000000 --- a/hid/arduino/lib/drivers/usb-keymap.h.mako +++ /dev/null @@ -1,37 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -<%! import operator %> -uint8_t keymapUsb(uint8_t code) { - switch (code) { -% for km in sorted(keymap, key=operator.attrgetter("mcu_code")): - % if km.usb_key.is_modifier: - case ${km.mcu_code}: return ${km.usb_key.arduino_modifier_code}; // ${km.web_name} - % else: - case ${km.mcu_code}: return ${km.usb_key.code}; // ${km.web_name} - % endif -% endfor - default: return 0; - } -} diff --git a/hid/arduino/patch.py b/hid/arduino/patch.py deleted file mode 100644 index c7674759..00000000 --- a/hid/arduino/patch.py +++ /dev/null @@ -1,49 +0,0 @@ -# https://docs.platformio.org/en/latest/projectconf/advanced_scripting.html - - -from os.path import exists -from os.path import join -from os.path import basename - -from typing import Dict - -Import("env") - - -# ===== -def _get_pkg_path(name: str) -> str: - path = env.PioPlatform().get_package_dir(name) - assert exists(path) - return path - - -def _get_libs() -> Dict[str, str]: - return { - builder.name: builder.path - for builder in env.GetLibBuilders() - } - - -def _patch(path: str, patch_path: str) -> None: - assert exists(path) - flag_path: str = join(path, f".{basename(patch_path)}.done") - if not exists(flag_path): - # TODO check for failure - env.Execute(f"patch -p1 -d {path} < {patch_path}") - env.Execute(lambda *_, **__: open(flag_path, "w").close()) - - -# ===== -if env.GetProjectOption("platform") == "ststm32": - _patch(_get_pkg_path("framework-arduinoststm32-maple"), "patches/platformio-stm32f1-no-serial-usb.patch") -elif env.GetProjectOption("platform") == "atmelavr": - _patch(_get_pkg_path("framework-arduino-avr"), "patches/arduino-main-no-usb.patch") - _patch(_get_pkg_path("framework-arduino-avr"), "patches/arduino-optional-cdc.patch") - _patch(_get_pkg_path("framework-arduino-avr"), "patches/arduino-get-plugged-endpoint.patch") - - _libs = _get_libs() - _patch(_libs["HID-Project"], "patches/hid-shut-up.patch") - _patch(_libs["HID-Project"], "patches/hid-no-singletones.patch") - _patch(_libs["HID-Project"], "patches/hid-win98.patch") -else: - assert(False) diff --git a/hid/arduino/patches/arduino-get-plugged-endpoint.patch b/hid/arduino/patches/arduino-get-plugged-endpoint.patch deleted file mode 100644 index c32131ff..00000000 --- a/hid/arduino/patches/arduino-get-plugged-endpoint.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- a/cores/arduino/PluggableUSB.h 2019-05-16 15:52:01.000000000 +0300 -+++ b/cores/arduino/PluggableUSB.h 2020-11-14 20:57:30.942432544 +0300 -@@ -31,6 +31,8 @@ - numEndpoints(numEps), numInterfaces(numIfs), endpointType(epType) - { } - -+ uint8_t getPluggedEndpoint() { return pluggedEndpoint; } -+ - protected: - virtual bool setup(USBSetup& setup) = 0; - virtual int getInterface(uint8_t* interfaceCount) = 0; diff --git a/hid/arduino/patches/arduino-main-no-usb.patch b/hid/arduino/patches/arduino-main-no-usb.patch deleted file mode 100644 index ab10c9a8..00000000 --- a/hid/arduino/patches/arduino-main-no-usb.patch +++ /dev/null @@ -1,24 +0,0 @@ -diff --git a/cores/arduino/main.cpp b/cores/arduino/main.cpp -index 434cd40..7aba76f 100644 ---- a/cores/arduino/main.cpp -+++ b/cores/arduino/main.cpp -@@ -36,15 +36,15 @@ int main(void) - - initVariant(); - --#if defined(USBCON) -- USBDevice.attach(); --#endif -+// #if defined(USBCON) -+// USBDevice.attach(); -+// #endif - - setup(); - - for (;;) { - loop(); -- if (serialEventRun) serialEventRun(); -+ // if (serialEventRun) serialEventRun(); - } - - return 0; diff --git a/hid/arduino/patches/arduino-optional-cdc.patch b/hid/arduino/patches/arduino-optional-cdc.patch deleted file mode 100644 index 08d8c078..00000000 --- a/hid/arduino/patches/arduino-optional-cdc.patch +++ /dev/null @@ -1,141 +0,0 @@ -From 8e823d276f939d79b2d323fad675fb8442a718c2 Mon Sep 17 00:00:00 2001 -From: Daniel Gibson -Date: Tue, 5 Jan 2021 13:48:43 +0100 -Subject: [PATCH] Allow disabling CDC with -DCDC_DISABLED - -https://github.com/arduino/ArduinoCore-avr/pull/383 - -Sometimes Arduino-based USB devices don't work because some hardware -(like KVM switches) gets confused by the CDC sub-devices. -This change makes it relatively easy to disable CDC at compiletime. -Disabling it of course means that the serial console won't work anymore, -so you need to use the reset button when flashing. - -CDC_DISABLED is also used in ArduinoCore-samd for the same purpose. - -based on -https://github.com/gdsports/usb-metamorph/tree/master/USBSerPassThruLine - -See also https://github.com/NicoHood/HID/issues/225 and -https://github.com/arduino/Arduino/issues/6387 and -https://forum.arduino.cc/index.php?topic=545288.msg3717028#msg3717028 ---- - cores/arduino/CDC.cpp | 8 ++++++++ - cores/arduino/USBCore.cpp | 18 +++++++++++++++++- - cores/arduino/USBDesc.h | 17 +++++++++++++++++ - 3 files changed, 42 insertions(+), 1 deletion(-) - -diff --git a/cores/arduino/CDC.cpp b/cores/arduino/CDC.cpp -index 4ff6b9b4..7d5afaab 100644 ---- a/cores/arduino/CDC.cpp -+++ b/cores/arduino/CDC.cpp -@@ -22,6 +22,13 @@ - - #if defined(USBCON) - -+#ifndef CDC_ENABLED -+ -+#warning "! Disabled serial console via USB (CDC)!" -+#warning "! With this change you'll have to use the Arduino's reset button/pin to flash (upload)!" -+ -+#else // CDC not disabled -+ - typedef struct - { - u32 dwDTERate; -@@ -299,4 +306,5 @@ int32_t Serial_::readBreak() { - - Serial_ Serial; - -+#endif /* if defined(CDC_ENABLED) */ - #endif /* if defined(USBCON) */ -diff --git a/cores/arduino/USBCore.cpp b/cores/arduino/USBCore.cpp -index dc6bc387..93352387 100644 ---- a/cores/arduino/USBCore.cpp -+++ b/cores/arduino/USBCore.cpp -@@ -69,8 +69,18 @@ const u8 STRING_MANUFACTURER[] PROGMEM = USB_MANUFACTURER; - #define DEVICE_CLASS 0x02 - - // DEVICE DESCRIPTOR -+ -+#ifdef CDC_ENABLED - const DeviceDescriptor USB_DeviceDescriptorIAD = - D_DEVICE(0xEF,0x02,0x01,64,USB_VID,USB_PID,0x100,IMANUFACTURER,IPRODUCT,ISERIAL,1); -+#else // CDC_DISABLED -+// The default descriptor uses USB class OxEF, subclass 0x02 with protocol 1 -+// which means "Interface Association Descriptor" - that's needed for the CDC, -+// but doesn't make much sense as a default for custom devices when CDC is disabled. -+// (0x00 means "Use class information in the Interface Descriptors" which should be generally ok) -+const DeviceDescriptor USB_DeviceDescriptorIAD = -+ D_DEVICE(0x00,0x00,0x00,64,USB_VID,USB_PID,0x100,IMANUFACTURER,IPRODUCT,ISERIAL,1); -+#endif - - //================================================================== - //================================================================== -@@ -328,10 +338,12 @@ int USB_Send(u8 ep, const void* d, int len) - u8 _initEndpoints[USB_ENDPOINTS] = - { - 0, // Control Endpoint -- -+ -+#ifdef CDC_ENABLED - EP_TYPE_INTERRUPT_IN, // CDC_ENDPOINT_ACM - EP_TYPE_BULK_OUT, // CDC_ENDPOINT_OUT - EP_TYPE_BULK_IN, // CDC_ENDPOINT_IN -+#endif - - // Following endpoints are automatically initialized to 0 - }; -@@ -373,10 +385,12 @@ void InitEndpoints() - static - bool ClassInterfaceRequest(USBSetup& setup) - { -+#ifdef CDC_ENABLED - u8 i = setup.wIndex; - - if (CDC_ACM_INTERFACE == i) - return CDC_Setup(setup); -+#endif - - #ifdef PLUGGABLE_USB_ENABLED - return PluggableUSB().setup(setup); -@@ -466,7 +480,9 @@ static u8 SendInterfaces() - { - u8 interfaces = 0; - -+#ifdef CDC_ENABLED - CDC_GetInterface(&interfaces); -+#endif - - #ifdef PLUGGABLE_USB_ENABLED - PluggableUSB().getInterface(&interfaces); -diff --git a/cores/arduino/USBDesc.h b/cores/arduino/USBDesc.h -index c0dce079..b55ac20b 100644 ---- a/cores/arduino/USBDesc.h -+++ b/cores/arduino/USBDesc.h -@@ -26,8 +26,25 @@ - - #define ISERIAL_MAX_LEN 20 - -+// Uncomment the following line or pass -DCDC_DISABLED to the compiler -+// to disable CDC (serial console via USB). -+// That's useful if you want to create an USB device (like an USB Boot Keyboard) -+// that works even with problematic devices (like KVM switches). -+// Keep in mind that with this change you'll have to use the Arduino's -+// reset button to be able to flash it. -+//#define CDC_DISABLED -+ -+#ifndef CDC_DISABLED -+#define CDC_ENABLED -+#endif -+ -+#ifdef CDC_ENABLED - #define CDC_INTERFACE_COUNT 2 - #define CDC_ENPOINT_COUNT 3 -+#else // CDC_DISABLED -+#define CDC_INTERFACE_COUNT 0 -+#define CDC_ENPOINT_COUNT 0 -+#endif - - #define CDC_ACM_INTERFACE 0 // CDC ACM - #define CDC_DATA_INTERFACE 1 // CDC Data diff --git a/hid/arduino/patches/hid-no-singletones.patch b/hid/arduino/patches/hid-no-singletones.patch deleted file mode 100644 index 85298448..00000000 --- a/hid/arduino/patches/hid-no-singletones.patch +++ /dev/null @@ -1,66 +0,0 @@ -diff -u -r a/src/SingleReport/BootKeyboard.cpp b/src/SingleReport/BootKeyboard.cpp ---- a/src/SingleReport/BootKeyboard.cpp 2019-07-13 21:16:23.000000000 +0300 -+++ b/src/SingleReport/BootKeyboard.cpp 2020-11-17 18:59:36.618815374 +0300 -@@ -206,6 +206,6 @@ - } - - --BootKeyboard_ BootKeyboard; -+//BootKeyboard_ BootKeyboard; - - -diff -u -r a/src/SingleReport/BootKeyboard.h b/src/SingleReport/BootKeyboard.h ---- a/src/SingleReport/BootKeyboard.h 2019-07-13 21:16:23.000000000 +0300 -+++ b/src/SingleReport/BootKeyboard.h 2020-11-17 19:00:54.967113649 +0300 -@@ -80,6 +80,6 @@ - uint8_t* featureReport; - int featureLength; - }; --extern BootKeyboard_ BootKeyboard; -+//extern BootKeyboard_ BootKeyboard; - - -diff -u -r a/src/SingleReport/BootMouse.cpp b/src/SingleReport/BootMouse.cpp ---- a/src/SingleReport/BootMouse.cpp 2019-07-13 21:16:23.000000000 +0300 -+++ b/src/SingleReport/BootMouse.cpp 2020-11-17 18:59:22.859113905 +0300 -@@ -139,6 +139,6 @@ - } - } - --BootMouse_ BootMouse; -+//BootMouse_ BootMouse; - - -diff -u -r a/src/SingleReport/BootMouse.h b/src/SingleReport/BootMouse.h ---- a/src/SingleReport/BootMouse.h 2019-07-13 21:16:23.000000000 +0300 -+++ b/src/SingleReport/BootMouse.h 2020-11-17 19:01:04.076915591 +0300 -@@ -48,6 +48,6 @@ - - virtual void SendReport(void* data, int length) override; - }; --extern BootMouse_ BootMouse; -+//extern BootMouse_ BootMouse; - - -diff -u -r a/src/SingleReport/SingleAbsoluteMouse.cpp b/src/SingleReport/SingleAbsoluteMouse.cpp ---- a/src/SingleReport/SingleAbsoluteMouse.cpp 2020-11-17 18:39:35.314843889 +0300 -+++ b/src/SingleReport/SingleAbsoluteMouse.cpp 2020-11-17 18:59:12.189345326 +0300 -@@ -139,6 +139,6 @@ - USB_Send(pluggedEndpoint | TRANSFER_RELEASE, data, length); - } - --SingleAbsoluteMouse_ SingleAbsoluteMouse; -+//SingleAbsoluteMouse_ SingleAbsoluteMouse; - - -diff -u -r a/src/SingleReport/SingleAbsoluteMouse.h b/src/SingleReport/SingleAbsoluteMouse.h ---- a/src/SingleReport/SingleAbsoluteMouse.h 2019-07-13 21:16:23.000000000 +0300 -+++ b/src/SingleReport/SingleAbsoluteMouse.h 2020-11-17 19:01:21.356539808 +0300 -@@ -49,6 +49,6 @@ - - virtual inline void SendReport(void* data, int length) override; - }; --extern SingleAbsoluteMouse_ SingleAbsoluteMouse; -+//extern SingleAbsoluteMouse_ SingleAbsoluteMouse; - - diff --git a/hid/arduino/patches/hid-shut-up.patch b/hid/arduino/patches/hid-shut-up.patch deleted file mode 100644 index 1de9defb..00000000 --- a/hid/arduino/patches/hid-shut-up.patch +++ /dev/null @@ -1,16 +0,0 @@ -diff --git a/src/KeyboardLayouts/ImprovedKeylayouts.h b/src/KeyboardLayouts/ImprovedKeylayouts.h -index 03b92a0..ee0bab4 100644 ---- a/src/KeyboardLayouts/ImprovedKeylayouts.h -+++ b/src/KeyboardLayouts/ImprovedKeylayouts.h -@@ -51,9 +51,9 @@ enum KeyboardLeds : uint8_t { - #ifndef HID_CUSTOM_LAYOUT - #define HID_CUSTOM_LAYOUT - #define LAYOUT_US_ENGLISH -- #pragma message "Using default ASCII layout for keyboard modules" -+ //#pragma message "Using default ASCII layout for keyboard modules" - #else -- #pragma message "Using custom layout for keyboard modules" -+ //#pragma message "Using custom layout for keyboard modules" - #endif - - // Hut1_12v2.pdf diff --git a/hid/arduino/patches/hid-win98.patch b/hid/arduino/patches/hid-win98.patch deleted file mode 100644 index 29670720..00000000 --- a/hid/arduino/patches/hid-win98.patch +++ /dev/null @@ -1,82 +0,0 @@ -From 82c8aee0ae3739eb22ae11b8f527d0c50b0ced31 Mon Sep 17 00:00:00 2001 -From: Maxim Devaev -Date: Sat, 14 Aug 2021 10:10:06 +0300 -Subject: [PATCH] Fixed absolute mouse positioning in Windows 98 - -Windows 98 contains a buggy HID driver. It allows you to move only -on the upper right quarter of the screen. Yes, this is indeed exceeding -the maximum value from the HID report. Yes, it really should work that way. - -VirtualBox has a similar fix, but with a shift in the other direction -(I didn't dig the details). I suppose it can be fixed somehow with a special -HID descriptor, but the proposed fix is simpler and really works. - -Related: https://github.com/pikvm/pikvm/issues/159 ---- - src/HID-APIs/AbsoluteMouseAPI.h | 3 +++ - src/HID-APIs/AbsoluteMouseAPI.hpp | 17 ++++++++++++++++- - 2 files changed, 19 insertions(+), 1 deletion(-) - -diff --git a/src/HID-APIs/AbsoluteMouseAPI.h b/src/HID-APIs/AbsoluteMouseAPI.h -index 66dbe42..c99e6a5 100644 ---- a/src/HID-APIs/AbsoluteMouseAPI.h -+++ b/src/HID-APIs/AbsoluteMouseAPI.h -@@ -57,6 +57,7 @@ class AbsoluteMouseAPI - int16_t xAxis; - int16_t yAxis; - uint8_t _buttons; -+ bool win98_fix; - inline void buttons(uint8_t b); - - inline int16_t qadd16(int16_t base, int16_t increment); -@@ -65,6 +66,8 @@ class AbsoluteMouseAPI - inline AbsoluteMouseAPI(void); - inline void begin(void); - inline void end(void); -+ inline void setWin98FixEnabled(bool enabled); -+ inline bool isWin98FixEnabled(void); - - inline void click(uint8_t b = MOUSE_LEFT); - inline void moveTo(int x, int y, signed char wheel = 0); -diff --git a/src/HID-APIs/AbsoluteMouseAPI.hpp b/src/HID-APIs/AbsoluteMouseAPI.hpp -index 0913c8a..0b9aaa2 100644 ---- a/src/HID-APIs/AbsoluteMouseAPI.hpp -+++ b/src/HID-APIs/AbsoluteMouseAPI.hpp -@@ -51,7 +51,7 @@ int16_t AbsoluteMouseAPI::qadd16(int16_t base, int16_t increment) { - } - - AbsoluteMouseAPI::AbsoluteMouseAPI(void): --xAxis(0), yAxis(0), _buttons(0) -+xAxis(0), yAxis(0), _buttons(0), win98_fix(false) - { - // Empty - } -@@ -66,6 +66,14 @@ void AbsoluteMouseAPI::end(void){ - moveTo(xAxis, yAxis, 0); - } - -+void AbsoluteMouseAPI::setWin98FixEnabled(bool enabled){ -+ win98_fix = enabled; -+} -+ -+bool AbsoluteMouseAPI::isWin98FixEnabled(void){ -+ return win98_fix; -+} -+ - void AbsoluteMouseAPI::click(uint8_t b){ - _buttons = b; - moveTo(xAxis, yAxis, 0); -@@ -84,6 +92,13 @@ void AbsoluteMouseAPI::moveTo(int x, int y, signed char wheel){ - // See detauls in AbsoluteMouse sources and here: https://github.com/NicoHood/HID/pull/306 - report.xAxis = ((int32_t)x + 32768) / 2; - report.yAxis = ((int32_t)y + 32768) / 2; -+ if (win98_fix) { -+ // Windows 98 contains a buggy driver. -+ // Yes, this is indeed exceeding the maximum value from the HID report. -+ // Yes, it really should work that way. -+ report.xAxis <<= 1; -+ report.yAxis <<= 1; -+ } - report.wheel = wheel; - SendReport(&report, sizeof(report)); - } diff --git a/hid/arduino/patches/platformio-stm32f1-no-serial-usb.patch b/hid/arduino/patches/platformio-stm32f1-no-serial-usb.patch deleted file mode 100644 index 547db168..00000000 --- a/hid/arduino/patches/platformio-stm32f1-no-serial-usb.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- aaa/tools/platformio-build-stm32f1.py 2022-07-16 18:54:42.536695468 +0200 -+++ bbb/tools/platformio-build-stm32f1.py 2022-07-16 19:03:10.988056751 +0200 -@@ -121,7 +121,7 @@ - env.Append( - CPPDEFINES=[ - ("CONFIG_MAPLE_MINI_NO_DISABLE_DEBUG", 1), -- "SERIAL_USB" -+ # "SERIAL_USB" - ]) - - is_generic = board.startswith("generic") or board == "hytiny_stm32f103t" diff --git a/hid/arduino/platformio-avr.ini b/hid/arduino/platformio-avr.ini deleted file mode 100644 index 8484ef50..00000000 --- a/hid/arduino/platformio-avr.ini +++ /dev/null @@ -1,119 +0,0 @@ -# http://docs.platformio.org/page/projectconf.html -[platformio] -core_dir = ./.platformio/ - -[env] -platform = atmelavr -board = micro -framework = arduino -lib_deps = - git+https://github.com/NicoHood/HID#2.8.2 - git+https://github.com/Harvie/ps2dev#v0.0.3 - digitalWriteFast@1.0.0 - HID@1.0 - drivers-avr -extra_scripts = - pre:avrdude.py - post:patch.py -platform_packages = - tool-avrdude - -[_common] -build_flags = - -DHID_USB_CHECK_ENDPOINT -# ----- The default config with dynamic switching ----- - -DHID_DYNAMIC - -DHID_WITH_USB - -DHID_SET_USB_KBD - -DHID_SET_USB_MOUSE_ABS -# ----- The USB ABS fix for Windows 98 (https://github.com/pikvm/pikvm/issues/159) ----- -# -DHID_WITH_USB_WIN98 -# ----- PS2 keyboard only ----- -# -DHID_WITH_PS2 -# -DHID_SET_PS2_KBD -# ----- PS2 keyboard + USB absolute mouse ----- -# -DHID_WITH_USB -# -DHID_WITH_PS2 -# -DHID_SET_PS2_KBD -# -DHID_SET_USB_MOUSE_ABS -# ----- PS2 keyboard + USB relative mouse ----- -# -DHID_WITH_USB -# -DHID_WITH_PS2 -# -DHID_SET_PS2_KBD -# -DHID_SET_USB_MOUSE_REL - -[_non_aum_pinout] = -build_flags = - -DHID_PS2_KBD_CLOCK_PIN=7 - -DHID_PS2_KBD_DATA_PIN=5 - - -# ===== Serial ===== -[env:serial] -extends = - _common - _non_aum_pinout -build_flags = - ${_common.build_flags} - ${_non_aum_pinout.build_flags} - -DCMD_SERIAL=Serial1 - -DCMD_SERIAL_SPEED=115200 - -DCMD_SERIAL_TIMEOUT=100000 -upload_port = /dev/ttyACM0 - - -# ===== RPi SPI ===== -[env:bootloader_spi] -upload_protocol = rpi -upload_flags = - -C - +avrdude-rpi.conf - -P - /dev/spidev0.0:/dev/gpiochip0 -extra_scripts = - pre:avrdude.py - -[_common_spi] -extends = - _common -build_flags = - ${_common.build_flags} - -DCMD_SPI - -DCDC_DISABLED -upload_protocol = custom -upload_flags = - -C - $PROJECT_PACKAGES_DIR/tool-avrdude/avrdude.conf - -C - +avrdude-rpi.conf - -P - /dev/spidev0.0:/dev/gpiochip0 - -c - rpi - -p - $BOARD_MCU -upload_command = avrdude $UPLOAD_FLAGS -U flash:w:$SOURCE:i - -[env:spi] -extends = - _common_spi - _non_aum_pinout -build_flags = - ${_common_spi.build_flags} - ${_non_aum_pinout.build_flags} - -[env:aum] -extends = - _common_spi -build_flags = - ${_common_spi.build_flags} - -DAUM - -DAUM_IS_USB_POWERED_PIN=A4 - -DAUM_SET_USB_VBUS_PIN=11 - -DAUM_SET_USB_CONNECTED_PIN=A5 - -DHID_PS2_KBD_VBUS_PIN=8 - -DHID_PS2_KBD_CLOCK_PIN=10 - -DHID_PS2_KBD_DATA_PIN=5 - -DHID_PS2_MOUSE_VBUS_PIN=6 - -DHID_PS2_MOUSE_CLOCK_PIN=9 - -DHID_PS2_MOUSE_DATA_PIN=13 diff --git a/hid/arduino/platformio-stm32.ini b/hid/arduino/platformio-stm32.ini deleted file mode 100644 index 94fd44d4..00000000 --- a/hid/arduino/platformio-stm32.ini +++ /dev/null @@ -1,52 +0,0 @@ -# http://docs.platformio.org/page/projectconf.html -[platformio] -core_dir = ./.platformio/ - -[env] -framework = arduino -platform = ststm32 -board = genericSTM32F103C8 -board_build.core = maple -extra_scripts = - post:patch.py - -[_common] -lib_deps = - git+https://github.com/ZulNs/STM32F1_RTC#v1.1.0 - git+https://github.com/arpruss/USBComposite_stm32f1#3c58f97eb006ee9cd1fb4fd55ac4faeeaead0974 - drivers-stm32 -build_flags = -# ----- The default config with dynamic switching ----- - -DHID_DYNAMIC - -DHID_WITH_USB - -DHID_SET_USB_KBD - -DHID_SET_USB_MOUSE_ABS - -[_serial] -extends = - _common -build_flags = - ${_common.build_flags} - -DCMD_SERIAL=Serial1 - -DCMD_SERIAL_SPEED=115200 - -DCMD_SERIAL_TIMEOUT=100000 -# ===== Serial ===== -[env:serial] -extends = - _serial -upload_flags = -c set CPUTAPID 0x2ba01477 -debug_tool= stlink -debug_build_flags = -Og -ggdb3 -g3 -debug_server = - .platformio/packages/tool-openocd/bin/openocd - -s .platformio/packages/tool-openocd/scripts - -f interface/stlink.cfg - -c "transport select hla_swd" - -c "set CPUTAPID 0x2ba01477" - -f target/stm32f1x.cfg - -c "reset_config none" -; build_type = debug -[env:patch] -; platformio-stm32f1-no-serial-usb.patch requires to running build again -; fake target was added to avoid error during first build -src_filter = - diff --git a/hid/arduino/src/main.cpp b/hid/arduino/src/main.cpp deleted file mode 100644 index aa880b10..00000000 --- a/hid/arduino/src/main.cpp +++ /dev/null @@ -1,253 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include - -#include "tools.h" -#include "proto.h" -#include "board.h" -#include "outputs.h" -#ifdef AUM -# include "aum.h" -#endif - - -static DRIVERS::Connection *_conn; -static DRIVERS::Board *_board; -static Outputs _out; - -#ifdef HID_DYNAMIC -# define RESET_TIMEOUT 500000 -static bool _reset_required = false; -static unsigned long _reset_timestamp; -#endif - - -// ----------------------------------------------------------------------------- -#ifdef HID_DYNAMIC -static void _resetRequest() { - _reset_required = true; - _reset_timestamp = micros(); -} -#endif - -static void _cmdSetKeyboard(const uint8_t *data) { // 1 bytes -# ifdef HID_DYNAMIC - _out.writeOutputs(PROTO::OUTPUTS1::KEYBOARD::MASK, data[0], false); - _resetRequest(); -# endif -} - -static void _cmdSetMouse(const uint8_t *data) { // 1 bytes -# ifdef HID_DYNAMIC - _out.writeOutputs(PROTO::OUTPUTS1::MOUSE::MASK, data[0], false); - _resetRequest(); -# endif -} - -static void _cmdSetConnected(const uint8_t *data) { // 1 byte -# ifdef AUM - aumSetUsbConnected(data[0]); -# endif -} - -static void _cmdClearHid(const uint8_t *_) { // 0 bytes - _out.kbd->clear(); - _out.mouse->clear(); -} - -static void _cmdKeyEvent(const uint8_t *data) { // 2 bytes - _out.kbd->sendKey(data[0], data[1]); -} - -static void _cmdMouseButtonEvent(const uint8_t *data) { // 2 bytes -# define MOUSE_PAIR(_state, _button) \ - _state & PROTO::CMD::MOUSE::_button::SELECT, \ - _state & PROTO::CMD::MOUSE::_button::STATE - _out.mouse->sendButtons( - MOUSE_PAIR(data[0], LEFT), - MOUSE_PAIR(data[0], RIGHT), - MOUSE_PAIR(data[0], MIDDLE), - MOUSE_PAIR(data[1], EXTRA_UP), - MOUSE_PAIR(data[1], EXTRA_DOWN) - ); -# undef MOUSE_PAIR -} - -static void _cmdMouseMoveEvent(const uint8_t *data) { // 4 bytes - // See /kvmd/apps/otg/hid/keyboard.py for details - _out.mouse->sendMove( - PROTO::merge8_int(data[0], data[1]), - PROTO::merge8_int(data[2], data[3]) - ); -} - -static void _cmdMouseRelativeEvent(const uint8_t *data) { // 2 bytes - _out.mouse->sendRelative(data[0], data[1]); -} - -static void _cmdMouseWheelEvent(const uint8_t *data) { // 2 bytes - // Y only, X is not supported - _out.mouse->sendWheel(data[1]); -} - -static uint8_t _handleRequest(const uint8_t *data) { // 8 bytes - _board->updateStatus(DRIVERS::RX_DATA); - // FIXME: See kvmd/kvmd#80 - // Should input buffer be cleared in this case? - if (data[0] == PROTO::MAGIC && PROTO::crc16(data, 6) == PROTO::merge8(data[6], data[7])) { -# define HANDLE(_handler) { _handler(data + 2); return PROTO::PONG::OK; } - switch (data[1]) { - case PROTO::CMD::PING: return PROTO::PONG::OK; - case PROTO::CMD::SET_KEYBOARD: HANDLE(_cmdSetKeyboard); - case PROTO::CMD::SET_MOUSE: HANDLE(_cmdSetMouse); - case PROTO::CMD::SET_CONNECTED: HANDLE(_cmdSetConnected); - case PROTO::CMD::CLEAR_HID: HANDLE(_cmdClearHid); - case PROTO::CMD::KEYBOARD::KEY: HANDLE(_cmdKeyEvent); - case PROTO::CMD::MOUSE::BUTTON: HANDLE(_cmdMouseButtonEvent); - case PROTO::CMD::MOUSE::MOVE: HANDLE(_cmdMouseMoveEvent); - case PROTO::CMD::MOUSE::RELATIVE: HANDLE(_cmdMouseRelativeEvent); - case PROTO::CMD::MOUSE::WHEEL: HANDLE(_cmdMouseWheelEvent); - case PROTO::CMD::REPEAT: return 0; - default: return PROTO::RESP::INVALID_ERROR; - } -# undef HANDLE - } - return PROTO::RESP::CRC_ERROR; -} - - -// ----------------------------------------------------------------------------- -static void _sendResponse(uint8_t code) { - static uint8_t prev_code = PROTO::RESP::NONE; - if (code == 0) { - code = prev_code; // Repeat the last code - } else { - prev_code = code; - } - - uint8_t response[8] = {0}; - response[0] = PROTO::MAGIC_RESP; - if (code & PROTO::PONG::OK) { - response[1] = PROTO::PONG::OK; -# ifdef HID_DYNAMIC - if (_reset_required) { - response[1] |= PROTO::PONG::RESET_REQUIRED; - if (is_micros_timed_out(_reset_timestamp, RESET_TIMEOUT)) { - _board->reset(); - } - } - response[2] = PROTO::OUTPUTS1::DYNAMIC; -# endif - if (_out.kbd->getType() != DRIVERS::DUMMY) { - if(_out.kbd->isOffline()) { - response[1] |= PROTO::PONG::KEYBOARD_OFFLINE; - } else { - _board->updateStatus(DRIVERS::KEYBOARD_ONLINE); - } - DRIVERS::KeyboardLedsState leds = _out.kbd->getLeds(); - response[1] |= (leds.caps ? PROTO::PONG::CAPS : 0); - response[1] |= (leds.num ? PROTO::PONG::NUM : 0); - response[1] |= (leds.scroll ? PROTO::PONG::SCROLL : 0); - switch (_out.kbd->getType()) { - case DRIVERS::USB_KEYBOARD: - response[2] |= PROTO::OUTPUTS1::KEYBOARD::USB; - break; - case DRIVERS::PS2_KEYBOARD: - response[2] |= PROTO::OUTPUTS1::KEYBOARD::PS2; - break; - } - } - if (_out.mouse->getType() != DRIVERS::DUMMY) { - if(_out.mouse->isOffline()) { - response[1] |= PROTO::PONG::MOUSE_OFFLINE; - } else { - _board->updateStatus(DRIVERS::MOUSE_ONLINE); - } - switch (_out.mouse->getType()) { - case DRIVERS::USB_MOUSE_ABSOLUTE_WIN98: - response[2] |= PROTO::OUTPUTS1::MOUSE::USB_WIN98; - break; - case DRIVERS::USB_MOUSE_ABSOLUTE: - response[2] |= PROTO::OUTPUTS1::MOUSE::USB_ABS; - break; - case DRIVERS::USB_MOUSE_RELATIVE: - response[2] |= PROTO::OUTPUTS1::MOUSE::USB_REL; - break; - } - } // TODO: ps2 -# ifdef AUM - response[3] |= PROTO::OUTPUTS2::CONNECTABLE; - if (aumIsUsbConnected()) { - response[3] |= PROTO::OUTPUTS2::CONNECTED; - } -# endif -# ifdef HID_WITH_USB - response[3] |= PROTO::OUTPUTS2::HAS_USB; -# ifdef HID_WITH_USB_WIN98 - response[3] |= PROTO::OUTPUTS2::HAS_USB_WIN98; -# endif -# endif -# ifdef HID_WITH_PS2 - response[3] |= PROTO::OUTPUTS2::HAS_PS2; -# endif - } else { - response[1] = code; - } - PROTO::split16(PROTO::crc16(response, 6), &response[6], &response[7]); - - _conn->write(response, 8); -} - -static void _onTimeout() { - _sendResponse(PROTO::RESP::TIMEOUT_ERROR); -} - -static void _onData(const uint8_t *data, size_t size) { - _sendResponse(_handleRequest(data)); -} - -void setup() { - _out.initOutputs(); - -# ifdef AUM - aumInit(); -# endif - - _conn = DRIVERS::Factory::makeConnection(DRIVERS::CONNECTION); - _conn->onTimeout(_onTimeout); - _conn->onData(_onData); - _conn->begin(); - - _board = DRIVERS::Factory::makeBoard(DRIVERS::BOARD); -} - -void loop() { -# ifdef AUM - aumProxyUsbVbus(); -# endif - - _out.kbd->periodic(); - _out.mouse->periodic(); - _board->periodic(); - _conn->periodic(); -} diff --git a/hid/arduino/src/outputs.h b/hid/arduino/src/outputs.h deleted file mode 100644 index b1992fc4..00000000 --- a/hid/arduino/src/outputs.h +++ /dev/null @@ -1,122 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include "factory.h" -#include "proto.h" - - -class Outputs { - public: - void writeOutputs(uint8_t mask, uint8_t outputs, bool force) { - int old = 0; - if (!force) { - old = _readOutputs(); - if (old < 0) { - old = 0; - } - } - uint8_t data[8] = {0}; - data[0] = PROTO::MAGIC; - data[1] = (old & ~mask) | outputs; - PROTO::split16(PROTO::crc16(data, 6), &data[6], &data[7]); - _storage->updateBlock(data, 0, 8); - } - - void initOutputs() { -# ifdef HID_DYNAMIC - _storage = DRIVERS::Factory::makeStorage(DRIVERS::NON_VOLATILE_STORAGE); -# else - _storage = DRIVERS::Factory::makeStorage(DRIVERS::DUMMY); -# endif - - int outputs = _readOutputs(); - if (outputs < 0) { - outputs = 0; -# if defined(HID_WITH_USB) && defined(HID_SET_USB_KBD) - outputs |= PROTO::OUTPUTS1::KEYBOARD::USB; -# elif defined(HID_WITH_PS2) && defined(HID_SET_PS2_KBD) - outputs |= PROTO::OUTPUTS1::KEYBOARD::PS2; -# endif -# if defined(HID_WITH_USB) && defined(HID_SET_USB_MOUSE_ABS) - outputs |= PROTO::OUTPUTS1::MOUSE::USB_ABS; -# elif defined(HID_WITH_USB) && defined(HID_SET_USB_MOUSE_REL) - outputs |= PROTO::OUTPUTS1::MOUSE::USB_REL; -# elif defined(HID_WITH_PS2) && defined(HID_SET_PS2_MOUSE) - outputs |= PROTO::OUTPUTS1::MOUSE::PS2; -# elif defined(HID_WITH_USB) && defined(HID_WITH_USB_WIN98) && defined(HID_SET_USB_MOUSE_WIN98) - outputs |= PROTO::OUTPUTS1::MOUSE::USB_WIN98; -# endif - writeOutputs(0xFF, outputs, true); - } - - uint8_t kbd_type = outputs & PROTO::OUTPUTS1::KEYBOARD::MASK; - switch (kbd_type) { - case PROTO::OUTPUTS1::KEYBOARD::USB: - kbd = DRIVERS::Factory::makeKeyboard(DRIVERS::USB_KEYBOARD); - break; - case PROTO::OUTPUTS1::KEYBOARD::PS2: - kbd = DRIVERS::Factory::makeKeyboard(DRIVERS::PS2_KEYBOARD); - break; - default: - kbd = DRIVERS::Factory::makeKeyboard(DRIVERS::DUMMY); - break; - } - - uint8_t mouse_type = outputs & PROTO::OUTPUTS1::MOUSE::MASK; - switch (mouse_type) { - case PROTO::OUTPUTS1::MOUSE::USB_ABS: - mouse = DRIVERS::Factory::makeMouse(DRIVERS::USB_MOUSE_ABSOLUTE); - break; - case PROTO::OUTPUTS1::MOUSE::USB_WIN98: - mouse = DRIVERS::Factory::makeMouse(DRIVERS::USB_MOUSE_ABSOLUTE_WIN98); - break; - case PROTO::OUTPUTS1::MOUSE::USB_REL: - mouse = DRIVERS::Factory::makeMouse(DRIVERS::USB_MOUSE_RELATIVE); - break; - default: - mouse = DRIVERS::Factory::makeMouse(DRIVERS::DUMMY); - break; - } - -# ifdef ARDUINO_ARCH_AVR - USBDevice.attach(); -# endif - - kbd->begin(); - mouse->begin(); - } - - DRIVERS::Keyboard *kbd = nullptr; - DRIVERS::Mouse *mouse = nullptr; - - private: - int _readOutputs(void) { - uint8_t data[8]; - _storage->readBlock(data, 0, 8); - if (data[0] != PROTO::MAGIC || PROTO::crc16(data, 6) != PROTO::merge8(data[6], data[7])) { - return -1; - } - return data[1]; - } - - DRIVERS::Storage *_storage = nullptr; -}; diff --git a/hid/arduino/src/proto.h b/hid/arduino/src/proto.h deleted file mode 100644 index a2e95722..00000000 --- a/hid/arduino/src/proto.h +++ /dev/null @@ -1,142 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - - -namespace PROTO { - const uint8_t MAGIC = 0x33; - const uint8_t MAGIC_RESP = 0x34; - - namespace RESP { // Plain responses - // const uint8_t OK = 0x20; // Legacy - const uint8_t NONE = 0x24; - const uint8_t CRC_ERROR = 0x40; - const uint8_t INVALID_ERROR = 0x45; - const uint8_t TIMEOUT_ERROR = 0x48; - }; - - namespace PONG { // Complex response - const uint8_t OK = 0x80; - const uint8_t CAPS = 0b00000001; - const uint8_t SCROLL = 0b00000010; - const uint8_t NUM = 0b00000100; - const uint8_t KEYBOARD_OFFLINE = 0b00001000; - const uint8_t MOUSE_OFFLINE = 0b00010000; - const uint8_t RESET_REQUIRED = 0b01000000; - }; - - namespace OUTPUTS1 { // Complex request/responce flags - const uint8_t DYNAMIC = 0b10000000; - namespace KEYBOARD { - const uint8_t MASK = 0b00000111; - const uint8_t USB = 0b00000001; - const uint8_t PS2 = 0b00000011; - }; - namespace MOUSE { - const uint8_t MASK = 0b00111000; - const uint8_t USB_ABS = 0b00001000; - const uint8_t USB_REL = 0b00010000; - const uint8_t PS2 = 0b00011000; - const uint8_t USB_WIN98 = 0b00100000; - }; - }; - - namespace OUTPUTS2 { // Complex response - const uint8_t CONNECTABLE = 0b10000000; - const uint8_t CONNECTED = 0b01000000; - const uint8_t HAS_USB = 0b00000001; - const uint8_t HAS_PS2 = 0b00000010; - const uint8_t HAS_USB_WIN98 = 0b00000100; - } - - namespace CMD { - const uint8_t PING = 0x01; - const uint8_t REPEAT = 0x02; - const uint8_t SET_KEYBOARD = 0x03; - const uint8_t SET_MOUSE = 0x04; - const uint8_t SET_CONNECTED = 0x05; - const uint8_t CLEAR_HID = 0x10; - - namespace KEYBOARD { - const uint8_t KEY = 0x11; - }; - - namespace MOUSE { - const uint8_t MOVE = 0x12; - const uint8_t BUTTON = 0x13; - const uint8_t WHEEL = 0x14; - const uint8_t RELATIVE = 0x15; - namespace LEFT { - const uint8_t SELECT = 0b10000000; - const uint8_t STATE = 0b00001000; - }; - namespace RIGHT { - const uint8_t SELECT = 0b01000000; - const uint8_t STATE = 0b00000100; - }; - namespace MIDDLE { - const uint8_t SELECT = 0b00100000; - const uint8_t STATE = 0b00000010; - }; - namespace EXTRA_UP { - const uint8_t SELECT = 0b10000000; - const uint8_t STATE = 0b00001000; - }; - namespace EXTRA_DOWN { - const uint8_t SELECT = 0b01000000; - const uint8_t STATE = 0b00000100; - }; - }; - }; - - uint16_t crc16(const uint8_t *buffer, unsigned length) { - const uint16_t polinom = 0xA001; - uint16_t crc = 0xFFFF; - - for (unsigned byte_count = 0; byte_count < length; ++byte_count) { - crc = crc ^ buffer[byte_count]; - for (unsigned bit_count = 0; bit_count < 8; ++bit_count) { - if ((crc & 0x0001) == 0) { - crc = crc >> 1; - } else { - crc = crc >> 1; - crc = crc ^ polinom; - } - } - } - return crc; - } - - inline int merge8_int(uint8_t from_a, uint8_t from_b) { - return (((int)from_a << 8) | (int)from_b); - } - - inline uint16_t merge8(uint8_t from_a, uint8_t from_b) { - return (((uint16_t)from_a << 8) | (uint16_t)from_b); - } - - inline void split16(uint16_t from, uint8_t *to_a, uint8_t *to_b) { - *to_a = (uint8_t)(from >> 8); - *to_b = (uint8_t)(from & 0xFF); - } -}; diff --git a/hid/pico/.gitignore b/hid/pico/.gitignore deleted file mode 100644 index 6d0c9883..00000000 --- a/hid/pico/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/.pico-sdk* -/.tinyusb* -/.ps2x2pico* -/.build/ -/*.uf2 diff --git a/hid/pico/CMakeLists.txt b/hid/pico/CMakeLists.txt deleted file mode 100644 index 58a4c1a6..00000000 --- a/hid/pico/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -cmake_minimum_required(VERSION 3.13) - -set(PICO_SDK_PATH ${CMAKE_CURRENT_LIST_DIR}/.pico-sdk) -set(PICO_TINYUSB_PATH ${CMAKE_CURRENT_LIST_DIR}/.tinyusb) -set(PS2_PATH ${CMAKE_CURRENT_LIST_DIR}/.ps2x2pico/src) - -# For TinyUSB -set(FAMILY rp2040) - -# Include pico_sdk_import.cmake from pico-sdk (instead of copying) -include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) - -# Generic setup -set(PROJECT hid) -project(${PROJECT}) - -# Initialize Pico-SDK -pico_sdk_init() - -# Set the path to the source code to build -set(SRC_TO_BUILD_PATH ${CMAKE_CURRENT_LIST_DIR}/src) -add_subdirectory(${SRC_TO_BUILD_PATH}) diff --git a/hid/pico/Makefile b/hid/pico/Makefile deleted file mode 100644 index cd1703d9..00000000 --- a/hid/pico/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -all: deps - rm -f hid.uf2 - cmake -B .build - cmake --build .build --config Release -- -j - ln .build/src/hid.uf2 . - - -upload: install -install: all - sudo mount /dev/sda1 /mnt - sudo cp hid.uf2 /mnt - sudo umount /mnt - - -clean: - rm -rf .build hid.uf2 -clean-all: clean - rm -rf .pico-sdk* .tinyusb* .ps2x2pico - - -define libdep - rm -rf .$(1).tmp - git clone https://github.com/$(2) .$(1).tmp - cd .$(1).tmp \ - && git checkout $(3) \ - && (test ! -f .gitmodules || git submodule update --init) - mv .$(1).tmp .$(1) -endef -.pico-sdk: - $(call libdep,pico-sdk,raspberrypi/pico-sdk,6a7db34ff63345a7badec79ebea3aaef1712f374) -.tinyusb: - $(call libdep,tinyusb,hathach/tinyusb,d713571cd44f05d2fc72efc09c670787b74106e0) -.ps2x2pico: - $(call libdep,ps2x2pico,No0ne/ps2x2pico,404aaf02949d5bee8013e3b5d0b3239abf6e13bd) -deps: .pico-sdk .tinyusb .ps2x2pico - - -.PHONY: deps diff --git a/hid/pico/src/CMakeLists.txt b/hid/pico/src/CMakeLists.txt deleted file mode 100644 index 7eeffa18..00000000 --- a/hid/pico/src/CMakeLists.txt +++ /dev/null @@ -1,39 +0,0 @@ -set(target_name hid) -add_executable(${target_name}) - -target_sources(${target_name} PRIVATE - main.c - ph_outputs.c - ph_usb.c - ph_usb_kbd.c - ph_usb_mouse.c - ph_ps2.c - ph_cmds.c - ph_com.c - ph_com_bridge.c - ph_com_spi.c - ph_com_uart.c - ph_debug.c - - ${PS2_PATH}/ps2out.c - ${PS2_PATH}/ps2in.c - ${PS2_PATH}/ps2kb.c - ${PS2_PATH}/ps2ms.c - ${PS2_PATH}/scancodesets.c -) -target_link_options(${target_name} PRIVATE -Xlinker --print-memory-usage) -target_compile_options(${target_name} PRIVATE -Wall -Wextra) -target_include_directories(${target_name} PRIVATE ${CMAKE_CURRENT_LIST_DIR} ${PS2_PATH}) - -pico_generate_pio_header(${target_name} ${PS2_PATH}/ps2out.pio) -pico_generate_pio_header(${target_name} ${PS2_PATH}/ps2in.pio) - -target_link_libraries(${target_name} PRIVATE - pico_stdlib - pico_unique_id - hardware_pio - hardware_spi - hardware_watchdog - tinyusb_device -) -pico_add_extra_outputs(${target_name}) diff --git a/hid/pico/src/main.c b/hid/pico/src/main.c deleted file mode 100644 index ee603db3..00000000 --- a/hid/pico/src/main.c +++ /dev/null @@ -1,130 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include "pico/stdlib.h" -#include "hardware/gpio.h" -#include "hardware/watchdog.h" - -#include "ph_types.h" -#include "ph_tools.h" -#include "ph_outputs.h" -#include "ph_usb.h" -#include "ph_ps2.h" -#include "ph_com.h" -#include "ph_proto.h" -#include "ph_cmds.h" -#include "ph_debug.h" - - -static bool _reset_required = false; - - -static u8 _handle_request(const u8 *data) { // 8 bytes - // FIXME: See kvmd/kvmd#80 - // Should input buffer be cleared in this case? - if (data[0] == PH_PROTO_MAGIC && ph_crc16(data, 6) == ph_merge8_u16(data[6], data[7])) { -# define HANDLE(x_handler, x_reset) { \ - x_handler(data + 2); \ - if (x_reset) { _reset_required = true; } \ - return PH_PROTO_PONG_OK; \ - } - switch (data[1]) { - case PH_PROTO_CMD_PING: return PH_PROTO_PONG_OK; - case PH_PROTO_CMD_SET_KBD: HANDLE(ph_cmd_set_kbd, true); - case PH_PROTO_CMD_SET_MOUSE: HANDLE(ph_cmd_set_mouse, true); - case PH_PROTO_CMD_SET_CONNECTED: return PH_PROTO_PONG_OK; // Arduino AUM - case PH_PROTO_CMD_CLEAR_HID: HANDLE(ph_cmd_send_clear, false); - case PH_PROTO_CMD_KBD_KEY: HANDLE(ph_cmd_kbd_send_key, false); - case PH_PROTO_CMD_MOUSE_BUTTON: HANDLE(ph_cmd_mouse_send_button, false); - case PH_PROTO_CMD_MOUSE_ABS: HANDLE(ph_cmd_mouse_send_abs, false); - case PH_PROTO_CMD_MOUSE_REL: HANDLE(ph_cmd_mouse_send_rel, false); - case PH_PROTO_CMD_MOUSE_WHEEL: HANDLE(ph_cmd_mouse_send_wheel, false); - case PH_PROTO_CMD_REPEAT: return 0; - } -# undef HANDLE - return PH_PROTO_RESP_INVALID_ERROR; - } - return PH_PROTO_RESP_CRC_ERROR; -} - -static void _send_response(u8 code) { - static u8 prev_code = PH_PROTO_RESP_NONE; - if (code == 0) { - code = prev_code; // Repeat the last code - } else { - prev_code = code; - } - - u8 resp[8] = {0}; - resp[0] = PH_PROTO_MAGIC_RESP; - - if (code & PH_PROTO_PONG_OK) { - resp[1] = PH_PROTO_PONG_OK; - if (_reset_required) { - resp[1] |= PH_PROTO_PONG_RESET_REQUIRED; - } - resp[2] = PH_PROTO_OUT1_DYNAMIC; - - resp[1] |= ph_cmd_get_offlines(); - resp[1] |= ph_cmd_kbd_get_leds(); - resp[2] |= ph_g_outputs_active; - resp[3] |= ph_g_outputs_avail; - } else { - resp[1] = code; - } - - ph_split16(ph_crc16(resp, 6), &resp[6], &resp[7]); - - ph_com_write(resp); - - if (_reset_required) { - watchdog_reboot(0, 0, 100); // Даем немного времени чтобы отправить ответ, а потом ребутимся - } -} - -static void _data_handler(const u8 *data) { - _send_response(_handle_request(data)); -} - -static void _timeout_handler(void) { - _send_response(PH_PROTO_RESP_TIMEOUT_ERROR); -} - - -int main(void) { - //ph_debug_act_init(); - //ph_debug_uart_init(); - ph_outputs_init(); - ph_ps2_init(); - ph_usb_init(); // Тут может быть инициализация USB-CDC для бриджа - ph_com_init(_data_handler, _timeout_handler); - - while (true) { - ph_usb_task(); - ph_ps2_task(); - if (!_reset_required) { - ph_com_task(); - //ph_debug_act_pulse(100); - } - } - return 0; -} diff --git a/hid/pico/src/ph_cmds.c b/hid/pico/src/ph_cmds.c deleted file mode 100644 index 52ccba16..00000000 --- a/hid/pico/src/ph_cmds.c +++ /dev/null @@ -1,131 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#include "tusb.h" - -#include "ph_types.h" -#include "ph_tools.h" -#include "ph_proto.h" -#include "ph_outputs.h" -#include "ph_usb.h" -#include "ph_usb_keymap.h" -#include "ph_ps2.h" - - -u8 ph_cmd_kbd_get_leds(void) { - u8 leds = 0; - if (PH_O_IS_KBD_USB) { - leds = ph_g_usb_kbd_leds; - } else if (PH_O_IS_KBD_PS2) { - leds = ph_g_ps2_kbd_leds; - } -# define GET(x_mod) ((leds & KEYBOARD_LED_##x_mod##LOCK) ? PH_PROTO_PONG_##x_mod : 0) - return (GET(CAPS) | GET(SCROLL) | GET(NUM)); -# undef GET -} - -u8 ph_cmd_get_offlines(void) { - bool kbd_online = true; - if (PH_O_IS_KBD_USB) { - kbd_online = ph_g_usb_kbd_online; - } else if (PH_O_IS_KBD_PS2) { - kbd_online = ph_g_ps2_kbd_online; - } - bool mouse_online = true; - if (PH_O_IS_MOUSE_USB) { - mouse_online = ph_g_usb_mouse_online; - } else if (PH_O_IS_MOUSE_PS2) { - mouse_online = ph_g_ps2_mouse_online; - } - return ( - (kbd_online ? 0 : PH_PROTO_PONG_KBD_OFFLINE) - | (mouse_online ? 0 : PH_PROTO_PONG_MOUSE_OFFLINE) - ); -} - -void ph_cmd_set_kbd(const u8 *args) { // 1 byte - ph_outputs_write(PH_PROTO_OUT1_KBD_MASK, args[0], false); -} - -void ph_cmd_set_mouse(const u8 *args) { // 1 byte - ph_outputs_write(PH_PROTO_OUT1_MOUSE_MASK, args[0], false); -} - -void ph_cmd_send_clear(const u8 *args) { // 0 bytes - (void)args; - ph_usb_send_clear(); - ph_ps2_send_clear(); -} - -void ph_cmd_kbd_send_key(const u8 *args) { // 2 bytes - const u8 key = ph_usb_keymap(args[0]); - if (key > 0) { - if (PH_O_IS_KBD_USB) { - ph_usb_kbd_send_key(key, args[1]); - } else if (PH_O_IS_KBD_PS2) { - ph_ps2_kbd_send_key(key, args[1]); - } - } -} - -void ph_cmd_mouse_send_button(const u8 *args) { // 2 bytes -# define HANDLE(x_byte_n, x_button) { \ - if (args[x_byte_n] & PH_PROTO_CMD_MOUSE_##x_button##_SELECT) { \ - const bool m_state = !!(args[x_byte_n] & PH_PROTO_CMD_MOUSE_##x_button##_STATE); \ - if (PH_O_IS_MOUSE_USB) { \ - ph_usb_mouse_send_button(MOUSE_BUTTON_##x_button, m_state); \ - } else if (PH_O_IS_MOUSE_PS2) { \ - ph_ps2_mouse_send_button(MOUSE_BUTTON_##x_button, m_state); \ - } \ - } \ - } - HANDLE(0, LEFT); - HANDLE(0, RIGHT); - HANDLE(0, MIDDLE); - HANDLE(1, BACKWARD); - HANDLE(1, FORWARD); -# undef HANDLE -} - -void ph_cmd_mouse_send_abs(const u8 *args) { // 4 bytes - if (PH_O_IS_MOUSE_USB_ABS) { - const s16 x = ph_merge8_s16(args[0], args[1]); - const s16 y = ph_merge8_s16(args[2], args[3]); - ph_usb_mouse_send_abs(x, y); - } -} - -void ph_cmd_mouse_send_rel(const u8 *args) { // 2 bytes - if (PH_O_IS_MOUSE_USB_REL) { - ph_usb_mouse_send_rel(args[0], args[1]); - } else if (PH_O_IS_MOUSE_PS2) { - ph_ps2_mouse_send_rel(args[0], args[1]); - } -} - -void ph_cmd_mouse_send_wheel(const u8 *args) { // 2 bytes - if (PH_O_IS_MOUSE_USB) { - ph_usb_mouse_send_wheel(args[0], args[1]); - } else if (PH_O_IS_MOUSE_PS2) { - ph_ps2_mouse_send_wheel(args[0], args[1]); - } -} diff --git a/hid/pico/src/ph_cmds.h b/hid/pico/src/ph_cmds.h deleted file mode 100644 index e0c4c9db..00000000 --- a/hid/pico/src/ph_cmds.h +++ /dev/null @@ -1,39 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#pragma once - -#include "ph_types.h" - - -u8 ph_cmd_kbd_get_leds(void); -u8 ph_cmd_get_offlines(void); - -void ph_cmd_set_kbd(const u8 *args); -void ph_cmd_set_mouse(const u8 *args); - -void ph_cmd_send_clear(const u8 *args); -void ph_cmd_kbd_send_key(const u8 *args); -void ph_cmd_mouse_send_button(const u8 *args); -void ph_cmd_mouse_send_abs(const u8 *args); -void ph_cmd_mouse_send_rel(const u8 *args); -void ph_cmd_mouse_send_wheel(const u8 *args); diff --git a/hid/pico/src/ph_com.c b/hid/pico/src/ph_com.c deleted file mode 100644 index 860b6202..00000000 --- a/hid/pico/src/ph_com.c +++ /dev/null @@ -1,67 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include "ph_com.h" - -#include "pico/stdlib.h" -#include "hardware/gpio.h" - -#include "ph_types.h" -#include "ph_outputs.h" -#include "ph_com_bridge.h" -#include "ph_com_spi.h" -#include "ph_com_uart.h" - - -#define _USE_SPI_PIN 22 - - -static bool _use_spi = true; - - -#define _COM(x_func, ...) { \ - if (ph_g_is_bridge) { \ - ph_com_bridge_##x_func(__VA_ARGS__); \ - } else if (_use_spi) { \ - ph_com_spi_##x_func(__VA_ARGS__); \ - } else { \ - ph_com_uart_##x_func(__VA_ARGS__); \ - } \ - } - - -void ph_com_init(void (*data_cb)(const u8 *), void (*timeout_cb)(void)) { - gpio_init(_USE_SPI_PIN); - gpio_set_dir(_USE_SPI_PIN, GPIO_IN); - gpio_pull_up(_USE_SPI_PIN); - sleep_ms(10); // Нужен небольшой слип для активации pull-up - _use_spi = gpio_get(_USE_SPI_PIN); - _COM(init, data_cb, timeout_cb); -} - -void ph_com_task(void) { - _COM(task); -} - -void ph_com_write(const u8 *data) { - _COM(write, data); -} diff --git a/hid/pico/src/ph_com.h b/hid/pico/src/ph_com.h deleted file mode 100644 index 12ca2d48..00000000 --- a/hid/pico/src/ph_com.h +++ /dev/null @@ -1,30 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "ph_types.h" - - -void ph_com_init(void (*data_cb)(const u8 *), void (*timeout_cb)(void)); -void ph_com_task(void); -void ph_com_write(const u8 *data); diff --git a/hid/pico/src/ph_com_bridge.c b/hid/pico/src/ph_com_bridge.c deleted file mode 100644 index d9fc4552..00000000 --- a/hid/pico/src/ph_com_bridge.c +++ /dev/null @@ -1,84 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include "ph_com_bridge.h" - -#include "pico/stdlib.h" - -#include "tusb.h" - -#include "ph_types.h" - - -#define _TIMEOUT_US 100000 - - -static u8 _buf[8] = {0}; -static u8 _index = 0; -static u64 _last_ts = 0; - -static void (*_data_cb)(const u8 *) = NULL; -static void (*_timeout_cb)(void) = NULL; - - -void ph_com_bridge_init(void (*data_cb)(const u8 *), void (*timeout_cb)(void)) { - _data_cb = data_cb; - _timeout_cb = timeout_cb; -} - -void ph_com_bridge_task(void) { - if (!tud_cdc_connected()) { - tud_cdc_write_clear(); - return; - } - - if (tud_cdc_available() > 0) { - const s32 ch = tud_cdc_read_char(); - if (ch < 0) { - goto no_data; - } - _buf[_index] = (u8)ch; - if (_index == 7) { - _data_cb(_buf); - _index = 0; - } else { - _last_ts = time_us_64(); - ++_index; - } - return; - } - - no_data: - if (_index > 0) { - if (_last_ts + _TIMEOUT_US < time_us_64()) { - _timeout_cb(); - _index = 0; - } - } -} - -void ph_com_bridge_write(const u8 *data) { - if (tud_cdc_connected()) { - tud_cdc_write(data, 8); - tud_cdc_write_flush(); - } -} diff --git a/hid/pico/src/ph_com_bridge.h b/hid/pico/src/ph_com_bridge.h deleted file mode 100644 index 7bc293e7..00000000 --- a/hid/pico/src/ph_com_bridge.h +++ /dev/null @@ -1,30 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "ph_types.h" - - -void ph_com_bridge_init(void (*data_cb)(const u8 *), void (*timeout_cb)(void)); -void ph_com_bridge_task(void); -void ph_com_bridge_write(const u8 *data); diff --git a/hid/pico/src/ph_com_spi.c b/hid/pico/src/ph_com_spi.c deleted file mode 100644 index e324e8dc..00000000 --- a/hid/pico/src/ph_com_spi.c +++ /dev/null @@ -1,121 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include "ph_com_spi.h" - -#include "hardware/gpio.h" -#include "hardware/irq.h" -#include "hardware/spi.h" -#include "hardware/regs/spi.h" - -#include "ph_types.h" - - -#define _BUS spi0 -#define _IRQ SPI0_IRQ -#define _FREQ (2 * 1000 * 1000) -#define _CS_PIN 21 -#define _RX_PIN 20 -#define _TX_PIN 19 -#define _CLK_PIN 18 - - -static volatile u8 _in_buf[8] = {0}; -static volatile u8 _in_index = 0; - -static volatile u8 _out_buf[8] = {0}; -static volatile u8 _out_index = 0; - -static void (*_data_cb)(const u8 *) = NULL; - - -static void _xfer_isr(void); - - -void ph_com_spi_init(void (*data_cb)(const u8 *), void (*timeout_cb)(void)) { - _data_cb = data_cb; - (void)timeout_cb; - - spi_init(_BUS, _FREQ); - spi_set_slave(_BUS, true); - spi_set_format(_BUS, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST); - - gpio_set_function(_CS_PIN, GPIO_FUNC_SPI); - gpio_set_function(_RX_PIN, GPIO_FUNC_SPI); - gpio_set_function(_TX_PIN, GPIO_FUNC_SPI); - gpio_set_function(_CLK_PIN, GPIO_FUNC_SPI); - - // https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2040/hardware_regs/include/hardware/regs/spi.h - irq_set_exclusive_handler(_IRQ, _xfer_isr); - spi_get_hw(_BUS)->imsc = SPI_SSPIMSC_RXIM_BITS | SPI_SSPIMSC_TXIM_BITS; - irq_set_enabled(_IRQ, true); -} - -void ph_com_spi_task(void) { - if (!_out_buf[0] && _in_index == 8) { - _data_cb((const u8 *)_in_buf); - } -} - -void ph_com_spi_write(const u8 *data) { - // Меджик в нулевом байте разрешает начать ответ - for (s8 i = 7; i >= 0; --i) { - _out_buf[i] = data[i]; - } -} - -void __isr __not_in_flash_func(_xfer_isr)(void) { -# define SR (spi_get_hw(_BUS)->sr) -# define DR (spi_get_hw(_BUS)->dr) - - while (SR & SPI_SSPSR_TNF_BITS) { - if (_out_buf[0] && _out_index < 8) { - DR = (u32)_out_buf[_out_index]; - ++_out_index; - if (_out_index == 8) { - _out_index = 0; - _in_index = 0; - _out_buf[0] = 0; - } - } else { - DR = (u32)0; - } - } - - while (SR & SPI_SSPSR_RNE_BITS) { - static bool receiving = false; - const u8 in = DR; - if (!receiving && in != 0) { - receiving = true; - } - if (receiving && _in_index < 8) { - _in_buf[_in_index] = in; - ++_in_index; - } - if (_in_index == 8) { - receiving = false; - } - } - -# undef DR -# undef SR -} diff --git a/hid/pico/src/ph_com_spi.h b/hid/pico/src/ph_com_spi.h deleted file mode 100644 index 9a6d55b6..00000000 --- a/hid/pico/src/ph_com_spi.h +++ /dev/null @@ -1,30 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "ph_types.h" - - -void ph_com_spi_init(void (*data_cb)(const u8 *), void (*timeout_cb)(void)); -void ph_com_spi_task(void); -void ph_com_spi_write(const u8 *data); diff --git a/hid/pico/src/ph_com_uart.c b/hid/pico/src/ph_com_uart.c deleted file mode 100644 index 8d64f56b..00000000 --- a/hid/pico/src/ph_com_uart.c +++ /dev/null @@ -1,75 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include "ph_com_uart.h" - -#include "pico/stdlib.h" -#include "hardware/gpio.h" -#include "hardware/uart.h" - -#include "ph_types.h" - - -#define _BUS uart1 -#define _SPEED 115200 -#define _RX_PIN 21 -#define _TX_PIN 20 -#define _TIMEOUT_US 100000 - - -static u8 _buf[8] = {0}; -static u8 _index = 0; -static u64 _last_ts = 0; - -static void (*_data_cb)(const u8 *) = NULL; -static void (*_timeout_cb)(void) = NULL; - - -void ph_com_uart_init(void (*data_cb)(const u8 *), void (*timeout_cb)(void)) { - _data_cb = data_cb; - _timeout_cb = timeout_cb; - uart_init(_BUS, _SPEED); - gpio_set_function(_RX_PIN, GPIO_FUNC_UART); - gpio_set_function(_TX_PIN, GPIO_FUNC_UART); -} - -void ph_com_uart_task(void) { - if (uart_is_readable(_BUS)) { - _buf[_index] = (u8)uart_getc(_BUS); - if (_index == 7) { - _data_cb(_buf); - _index = 0; - } else { - _last_ts = time_us_64(); - ++_index; - } - } else if (_index > 0) { - if (_last_ts + _TIMEOUT_US < time_us_64()) { - _timeout_cb(); - _index = 0; - } - } -} - -void ph_com_uart_write(const u8 *data) { - uart_write_blocking(_BUS, data, 8); -} diff --git a/hid/pico/src/ph_com_uart.h b/hid/pico/src/ph_com_uart.h deleted file mode 100644 index b423c6c8..00000000 --- a/hid/pico/src/ph_com_uart.h +++ /dev/null @@ -1,30 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "ph_types.h" - - -void ph_com_uart_init(void (*data_cb)(const u8 *), void (*timeout_cb)(void)); -void ph_com_uart_task(void); -void ph_com_uart_write(const u8 *data); diff --git a/hid/pico/src/ph_debug.c b/hid/pico/src/ph_debug.c deleted file mode 100644 index e08b9db1..00000000 --- a/hid/pico/src/ph_debug.c +++ /dev/null @@ -1,58 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#include "pico/stdlib.h" -#include "hardware/gpio.h" - -#include "ph_types.h" - - -#define _UART uart0 -#define _SPEED 3000000 -#define _RX_PIN -1 // 1 - No stdin -#define _TX_PIN 0 -#define _ACT_PIN 25 - - -void ph_debug_uart_init(void) { - stdio_uart_init_full(_UART, _SPEED, _TX_PIN, _RX_PIN); -} - -void ph_debug_act_init(void) { - gpio_init(_ACT_PIN); - gpio_set_dir(_ACT_PIN, GPIO_OUT); -} - -void ph_debug_act(bool flag) { - gpio_put(_ACT_PIN, flag); -} - -void ph_debug_act_pulse(u64 delay_ms) { - static bool flag = false; - static u64 next_ts = 0; - const u64 now_ts = time_us_64(); - if (now_ts >= next_ts) { - ph_debug_act(flag); - flag = !flag; - next_ts = now_ts + (delay_ms * 1000); - } -} diff --git a/hid/pico/src/ph_debug.h b/hid/pico/src/ph_debug.h deleted file mode 100644 index 33d5eed8..00000000 --- a/hid/pico/src/ph_debug.h +++ /dev/null @@ -1,31 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "ph_types.h" - - -void ph_debug_uart_init(); -void ph_debug_act_init(); -void ph_debug_act(bool flag); -void ph_debug_act_pulse(u64 delay_ms); diff --git a/hid/pico/src/ph_outputs.c b/hid/pico/src/ph_outputs.c deleted file mode 100644 index 4ec40de5..00000000 --- a/hid/pico/src/ph_outputs.c +++ /dev/null @@ -1,146 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#include "ph_outputs.h" - -#include "pico/stdlib.h" -#include "hardware/gpio.h" -#include "hardware/structs/watchdog.h" - -#include "ph_types.h" -#include "ph_tools.h" -#include "ph_proto.h" - - -#define _PS2_ENABLED_PIN 2 -#define _PS2_SET_KBD_PIN 3 -#define _PS2_SET_MOUSE_PIN 4 - -#define _BRIDGE_MODE_PIN 5 - -#define _USB_DISABLED_PIN 6 -#define _USB_ENABLE_W98_PIN 7 -#define _USB_SET_MOUSE_REL_PIN 8 -#define _USB_SET_MOUSE_W98_PIN 9 - - -u8 ph_g_outputs_active = 0; -u8 ph_g_outputs_avail = 0; -bool ph_g_is_bridge = false; - - -static int _read_outputs(void); - - -void ph_outputs_init(void) { -# define INIT_SWITCH(x_pin) { gpio_init(x_pin); gpio_set_dir(x_pin, GPIO_IN); gpio_pull_up(x_pin); } - INIT_SWITCH(_PS2_ENABLED_PIN); - INIT_SWITCH(_PS2_SET_KBD_PIN); - INIT_SWITCH(_PS2_SET_MOUSE_PIN); - - INIT_SWITCH(_BRIDGE_MODE_PIN); - - INIT_SWITCH(_USB_DISABLED_PIN); - INIT_SWITCH(_USB_ENABLE_W98_PIN); - INIT_SWITCH(_USB_SET_MOUSE_REL_PIN); - INIT_SWITCH(_USB_SET_MOUSE_W98_PIN); -# undef INIT_SWITCH - sleep_ms(10); // Нужен небольшой слип для активации pull-up - - const bool o_ps2_enabled = !gpio_get(_PS2_ENABLED_PIN); // Note: all pins are pulled up! - const bool o_ps2_kbd = !gpio_get(_PS2_SET_KBD_PIN); - const bool o_ps2_mouse = !gpio_get(_PS2_SET_MOUSE_PIN); - - ph_g_is_bridge = !gpio_get(_BRIDGE_MODE_PIN); - - const bool o_usb_disabled = (ph_g_is_bridge || !gpio_get(_USB_DISABLED_PIN)); - const bool o_usb_enabled_w98 = !gpio_get(_USB_ENABLE_W98_PIN); - const bool o_usb_mouse_rel = !gpio_get(_USB_SET_MOUSE_REL_PIN); - const bool o_usb_mouse_w98 = !gpio_get(_USB_SET_MOUSE_W98_PIN); - - int outputs = _read_outputs(); - if (outputs < 0) { - outputs = 0; - - if (o_ps2_enabled && (o_ps2_kbd || o_usb_disabled)) { - outputs |= PH_PROTO_OUT1_KBD_PS2; - } else if (!o_usb_disabled) { - outputs |= PH_PROTO_OUT1_KBD_USB; - } - - if (o_ps2_enabled && (o_ps2_mouse || o_usb_disabled)) { - outputs |= PH_PROTO_OUT1_MOUSE_PS2; - } else if (!o_usb_disabled) { - if (o_usb_enabled_w98 && o_usb_mouse_w98) { - outputs |= PH_PROTO_OUT1_MOUSE_USB_W98; - } else if (o_usb_mouse_rel) { - outputs |= PH_PROTO_OUT1_MOUSE_USB_REL; - } else { - outputs |= PH_PROTO_OUT1_MOUSE_USB_ABS; - } - } - - ph_outputs_write(0xFF, outputs, true); - } - - if (!o_usb_disabled) { - ph_g_outputs_avail |= PH_PROTO_OUT2_HAS_USB; - if (o_usb_enabled_w98) { - ph_g_outputs_avail |= PH_PROTO_OUT2_HAS_USB_W98; - } - } - if (o_ps2_enabled) { - ph_g_outputs_avail |= PH_PROTO_OUT2_HAS_PS2; - } - - ph_g_outputs_active = outputs & 0xFF; -} - -void ph_outputs_write(u8 mask, u8 outputs, bool force) { - int old = 0; - if (!force) { - old = _read_outputs(); - if (old < 0) { - old = 0; - } - } - u8 data[4] = {0}; - data[0] = PH_PROTO_MAGIC; - data[1] = (old & ~mask) | outputs; - ph_split16(ph_crc16(data, 2), &data[2], &data[3]); - const u32 s0 = ((u32)data[0] << 24) | ((u32)data[1] << 16) | ((u32)data[2] << 8) | (u32)data[3]; - watchdog_hw->scratch[0] = s0; -} - -static int _read_outputs(void) { - const u32 s0 = watchdog_hw->scratch[0]; - const u8 data[4] = { - (s0 >> 24) & 0xFF, - (s0 >> 16) & 0xFF, - (s0 >> 8) & 0xFF, - s0 & 0xFF, - }; - if (data[0] != PH_PROTO_MAGIC || ph_crc16(data, 2) != ph_merge8_u16(data[2], data[3])) { - return -1; - } - return data[1]; -} diff --git a/hid/pico/src/ph_outputs.h b/hid/pico/src/ph_outputs.h deleted file mode 100644 index bddf9208..00000000 --- a/hid/pico/src/ph_outputs.h +++ /dev/null @@ -1,46 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#pragma once - -#include "ph_types.h" -#include "ph_proto.h" - - -#define PH_O_HAS_PS2 (!!(ph_g_outputs_avail & PH_PROTO_OUT2_HAS_PS2)) -#define PH_O_KBD(x_id) ((ph_g_outputs_active & PH_PROTO_OUT1_KBD_MASK) == PH_PROTO_OUT1_KBD_##x_id) -#define PH_O_MOUSE(x_id) ((ph_g_outputs_active & PH_PROTO_OUT1_MOUSE_MASK) == PH_PROTO_OUT1_MOUSE_##x_id) -#define PH_O_IS_KBD_USB PH_O_KBD(USB) -#define PH_O_IS_MOUSE_USB (PH_O_MOUSE(USB_ABS) || PH_O_MOUSE(USB_REL) || PH_O_MOUSE(USB_W98)) -#define PH_O_IS_MOUSE_USB_ABS (PH_O_MOUSE(USB_ABS) || PH_O_MOUSE(USB_W98)) -#define PH_O_IS_MOUSE_USB_REL PH_O_MOUSE(USB_REL) -#define PH_O_IS_KBD_PS2 PH_O_KBD(PS2) -#define PH_O_IS_MOUSE_PS2 PH_O_MOUSE(PS2) - - -extern bool ph_g_is_bridge; -extern u8 ph_g_outputs_active; -extern u8 ph_g_outputs_avail; - - -void ph_outputs_init(void); -void ph_outputs_write(u8 mask, u8 outputs, bool force); diff --git a/hid/pico/src/ph_proto.h b/hid/pico/src/ph_proto.h deleted file mode 100644 index 78b28d32..00000000 --- a/hid/pico/src/ph_proto.h +++ /dev/null @@ -1,92 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "ph_types.h" - - -#define PH_PROTO_MAGIC ((u8)0x33) -#define PH_PROTO_MAGIC_RESP ((u8)0x34) - -//#define PH_PROTO_RESP_OK ((u8)0x20) // Legacy -#define PH_PROTO_RESP_NONE ((u8)0x24) -#define PH_PROTO_RESP_CRC_ERROR ((u8)0x40) -#define PH_PROTO_RESP_INVALID_ERROR ((u8)0x45) -#define PH_PROTO_RESP_TIMEOUT_ERROR ((u8)0x48) - -// Complex response flags -#define PH_PROTO_PONG_OK ((u8)0b10000000) -#define PH_PROTO_PONG_CAPS ((u8)0b00000001) -#define PH_PROTO_PONG_SCROLL ((u8)0b00000010) -#define PH_PROTO_PONG_NUM ((u8)0b00000100) -#define PH_PROTO_PONG_KBD_OFFLINE ((u8)0b00001000) -#define PH_PROTO_PONG_MOUSE_OFFLINE ((u8)0b00010000) -#define PH_PROTO_PONG_RESET_REQUIRED ((u8)0b01000000) - -// Complex request/response flags -#define PH_PROTO_OUT1_DYNAMIC ((u8)0b10000000) -#define PH_PROTO_OUT1_KBD_MASK ((u8)0b00000111) -#define PH_PROTO_OUT1_KBD_USB ((u8)0b00000001) -#define PH_PROTO_OUT1_KBD_PS2 ((u8)0b00000011) -// + -#define PH_PROTO_OUT1_MOUSE_MASK ((u8)0b00111000) -#define PH_PROTO_OUT1_MOUSE_USB_ABS ((u8)0b00001000) -#define PH_PROTO_OUT1_MOUSE_USB_REL ((u8)0b00010000) -#define PH_PROTO_OUT1_MOUSE_PS2 ((u8)0b00011000) -#define PH_PROTO_OUT1_MOUSE_USB_W98 ((u8)0b00100000) - -// Complex response -#define PH_PROTO_OUT2_CONNECTABLE ((u8)0b10000000) -#define PH_PROTO_OUT2_CONNECTED ((u8)0b01000000) -#define PH_PROTO_OUT2_HAS_USB ((u8)0b00000001) -#define PH_PROTO_OUT2_HAS_PS2 ((u8)0b00000010) -#define PH_PROTO_OUT2_HAS_USB_W98 ((u8)0b00000100) - -#define PH_PROTO_CMD_PING ((u8)0x01) -#define PH_PROTO_CMD_REPEAT ((u8)0x02) -#define PH_PROTO_CMD_SET_KBD ((u8)0x03) -#define PH_PROTO_CMD_SET_MOUSE ((u8)0x04) -#define PH_PROTO_CMD_SET_CONNECTED ((u8)0x05) -#define PH_PROTO_CMD_CLEAR_HID ((u8)0x10) -// + -#define PH_PROTO_CMD_KBD_KEY ((u8)0x11) -// + -#define PH_PROTO_CMD_MOUSE_ABS ((u8)0x12) -#define PH_PROTO_CMD_MOUSE_BUTTON ((u8)0x13) -#define PH_PROTO_CMD_MOUSE_WHEEL ((u8)0x14) -#define PH_PROTO_CMD_MOUSE_REL ((u8)0x15) -// + -#define PH_PROTO_CMD_MOUSE_LEFT_SELECT ((u8)0b10000000) -#define PH_PROTO_CMD_MOUSE_LEFT_STATE ((u8)0b00001000) -// + -#define PH_PROTO_CMD_MOUSE_RIGHT_SELECT ((u8)0b01000000) -#define PH_PROTO_CMD_MOUSE_RIGHT_STATE ((u8)0b00000100) -// + -#define PH_PROTO_CMD_MOUSE_MIDDLE_SELECT ((u8)0b00100000) -#define PH_PROTO_CMD_MOUSE_MIDDLE_STATE ((u8)0b00000010) -// + -#define PH_PROTO_CMD_MOUSE_BACKWARD_SELECT ((u8)0b10000000) // Previous/Up -#define PH_PROTO_CMD_MOUSE_BACKWARD_STATE ((u8)0b00001000) // Previous/Up -// + -#define PH_PROTO_CMD_MOUSE_FORWARD_SELECT ((u8)0b01000000) // Next/Down -#define PH_PROTO_CMD_MOUSE_FORWARD_STATE ((u8)0b00000100) // Next/Down diff --git a/hid/pico/src/ph_ps2.c b/hid/pico/src/ph_ps2.c deleted file mode 100644 index fcb4dfbb..00000000 --- a/hid/pico/src/ph_ps2.c +++ /dev/null @@ -1,143 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#include "ph_ps2.h" - -#include "ph_types.h" -#include "ph_outputs.h" - -#include "hardware/gpio.h" - - -#define _LS_POWER_PIN 13 -#define _KBD_DATA_PIN 11 // CLK == 12 -#define _MOUSE_DATA_PIN 14 // CLK == 15 - -#define _KBD_IN_DATA_PIN 26 // passthru, CLK == 27 -#define _MOUSE_IN_DATA_PIN 16 // passthru, CLK == 17 - - -u8 ph_g_ps2_kbd_leds = 0; -bool ph_g_ps2_kbd_online = 0; -bool ph_g_ps2_mouse_online = 0; - -u8 ph_ps2_kbd_modifiers = 0; -u8 ph_ps2_mouse_buttons = 0; - - -void tuh_kb_set_leds(u8 leds) { - ph_g_ps2_kbd_leds = leds; -} - -void ph_ps2_init(void) { - if (PH_O_HAS_PS2) { - gpio_init(_LS_POWER_PIN); - gpio_set_dir(_LS_POWER_PIN, GPIO_OUT); - gpio_put(_LS_POWER_PIN, true); - } - -# define INIT_STUB(x_pin) { \ - gpio_init(x_pin); gpio_set_dir(x_pin, GPIO_IN); \ - gpio_init(x_pin + 1); gpio_set_dir(x_pin + 1, GPIO_IN); \ - } - - if (PH_O_IS_KBD_PS2) { - kb_init(_KBD_DATA_PIN, _KBD_IN_DATA_PIN); - } else { - INIT_STUB(_KBD_DATA_PIN); - } - - if (PH_O_IS_MOUSE_PS2) { - ms_init(_MOUSE_DATA_PIN, _MOUSE_IN_DATA_PIN); - } else { - INIT_STUB(_MOUSE_DATA_PIN); - } - -# undef INIT_STUB -} - -void ph_ps2_task(void) { - if (PH_O_IS_KBD_PS2) { - ph_g_ps2_kbd_online = kb_task(); - } - - if (PH_O_IS_MOUSE_PS2) { - ph_g_ps2_mouse_online = ms_task(); - } -} - -void ph_ps2_kbd_send_key(u8 key, bool state) { - if (PH_O_IS_KBD_PS2) { - if (key >= 0xe0 && key <= 0xe7) { - if (state) { - ph_ps2_kbd_modifiers = ph_ps2_kbd_modifiers | (1 << (key - 0xe0)); - } else { - ph_ps2_kbd_modifiers = ph_ps2_kbd_modifiers & ~(1 << (key - 0xe0)); - } - } - - kb_send_key(key, state, ph_ps2_kbd_modifiers); - } -} - -void ph_ps2_mouse_send_button(u8 button, bool state) { - if (PH_O_IS_MOUSE_PS2) { - button--; - - if (state) { - ph_ps2_mouse_buttons = ph_ps2_mouse_buttons | (1 << button); - } else { - ph_ps2_mouse_buttons = ph_ps2_mouse_buttons & ~(1 << button); - } - - ms_send_movement(ph_ps2_mouse_buttons, 0, 0, 0); - } -} - -void ph_ps2_mouse_send_rel(s8 x, s8 y) { - if (PH_O_IS_MOUSE_PS2) { - ms_send_movement(ph_ps2_mouse_buttons, x, y, 0); - } -} - -void ph_ps2_mouse_send_wheel(s8 h, s8 v) { - if (PH_O_IS_MOUSE_PS2) { - (void)h; // as far as I know there is no standard way for horizontal scrolling - ms_send_movement(ph_ps2_mouse_buttons, 0, 0, v); - } -} - -void ph_ps2_send_clear(void) { - if (PH_O_IS_KBD_PS2) { - //for(u8 key = 0xe0; key <= 0xe7; key++) { - // kb_send_key(key, false, 0); - //} - - //for(u8 key = 4; key <= 116; key++) { - // kb_send_key(key, false, 0); - //} - } - - if (PH_O_IS_MOUSE_PS2) { - ms_send_movement(0, 0, 0, 0); - } -} diff --git a/hid/pico/src/ph_ps2.h b/hid/pico/src/ph_ps2.h deleted file mode 100644 index 69f55866..00000000 --- a/hid/pico/src/ph_ps2.h +++ /dev/null @@ -1,49 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#pragma once - -#include "ph_types.h" - - -extern u8 ph_g_ps2_kbd_leds; -extern bool ph_g_ps2_kbd_online; -extern bool ph_g_ps2_mouse_online; - - -void ph_ps2_init(void); -void ph_ps2_task(void); - -void tuh_kb_set_leds(u8 leds); -void kb_init(u8 gpio_out, u8 gpio_in); -bool kb_task(); -void kb_send_key(u8 key, bool state, u8 modifiers); -void ph_ps2_kbd_send_key(u8 key, bool state); - -void ms_init(u8 gpio_out, u8 gpio_in); -bool ms_task(); -void ms_send_movement(u8 buttons, s8 x, s8 y, s8 z); -void ph_ps2_mouse_send_button(u8 button, bool state); -void ph_ps2_mouse_send_rel(s8 x, s8 y); -void ph_ps2_mouse_send_wheel(s8 h, s8 v); - -void ph_ps2_send_clear(void); diff --git a/hid/pico/src/ph_tools.h b/hid/pico/src/ph_tools.h deleted file mode 100644 index 489302c4..00000000 --- a/hid/pico/src/ph_tools.h +++ /dev/null @@ -1,57 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "ph_types.h" - - -inline u16 ph_crc16(const u8 *buf, uz len) { - const u16 polinom = 0xA001; - u16 crc = 0xFFFF; - - for (uz byte_count = 0; byte_count < len; ++byte_count) { - crc = crc ^ buf[byte_count]; - for (uz bit_count = 0; bit_count < 8; ++bit_count) { - if ((crc & 0x0001) == 0) { - crc = crc >> 1; - } else { - crc = crc >> 1; - crc = crc ^ polinom; - } - } - } - return crc; -} - -inline s16 ph_merge8_s16(u8 a, u8 b) { - return (((int)a << 8) | (int)b); -} - -inline u16 ph_merge8_u16(u8 a, u8 b) { - return (((u16)a << 8) | (u16)b); -} - -inline void ph_split16(u16 from, u8 *to_a, u8 *to_b) { - *to_a = (u8)(from >> 8); - *to_b = (u8)(from & 0xFF); -} diff --git a/hid/pico/src/ph_types.h b/hid/pico/src/ph_types.h deleted file mode 100644 index 97b89e90..00000000 --- a/hid/pico/src/ph_types.h +++ /dev/null @@ -1,38 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#pragma once - -#include -#include -#include - - -typedef int8_t s8; -typedef int16_t s16; -typedef int32_t s32; - -typedef size_t uz; -typedef uint8_t u8; -typedef uint16_t u16; -typedef uint32_t u32; -typedef uint64_t u64; diff --git a/hid/pico/src/ph_usb.c b/hid/pico/src/ph_usb.c deleted file mode 100644 index a3d1da00..00000000 --- a/hid/pico/src/ph_usb.c +++ /dev/null @@ -1,437 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#include "ph_usb.h" - -#include -#include - -#include "pico/stdlib.h" -#include "pico/unique_id.h" - -#include "tusb.h" -#if TUD_OPT_HIGH_SPEED -# error "High-Speed is not supported" -#endif - -#include "ph_types.h" -#include "ph_outputs.h" -#include "ph_usb_kbd.h" -#include "ph_usb_mouse.h" - - -u8 ph_g_usb_kbd_leds = 0; -bool ph_g_usb_kbd_online = true; -bool ph_g_usb_mouse_online = true; - -static int _kbd_iface = -1; -static int _mouse_iface = -1; - -static u8 _kbd_mods = 0; -static u8 _kbd_keys[6] = {0}; -#define _KBD_CLEAR { _kbd_mods = 0; memset(_kbd_keys, 0, 6); } - -static u8 _mouse_buttons = 0; -static s16 _mouse_abs_x = 0; -static s16 _mouse_abs_y = 0; -#define _MOUSE_CLEAR { _mouse_buttons = 0; _mouse_abs_x = 0; _mouse_abs_y = 0; } - - -static void _kbd_sync_report(bool new); -static void _mouse_abs_send_report(s8 h, s8 v); -static void _mouse_rel_send_report(s8 x, s8 y, s8 h, s8 v); - - -void ph_usb_init(void) { - if (ph_g_is_bridge || PH_O_IS_KBD_USB || PH_O_IS_MOUSE_USB) { - tud_init(0); - } -} - -void ph_usb_task(void) { - if (ph_g_is_bridge || PH_O_IS_KBD_USB || PH_O_IS_MOUSE_USB) { - tud_task(); - - static u64 next_ts = 0; - const u64 now_ts = time_us_64(); - if (next_ts == 0 || now_ts >= next_ts) { -# define CHECK_IFACE(x_dev) \ - static u64 offline_ts = 0; \ - static bool prev_online = true; \ - const bool online = (tud_ready() && tud_hid_n_ready(_##x_dev##_iface)); \ - bool force = false; \ - if (online) { \ - if (!ph_g_usb_##x_dev##_online) { \ - force = true; /* Если был переход из долгого оффлайна в онлайн */ \ - } \ - ph_g_usb_##x_dev##_online = true; \ - offline_ts = 0; \ - } else if (prev_online && !online) { \ - offline_ts = now_ts; /* Начинаем отсчет для долгого оффлайна */ \ - } else if (!prev_online && !online && offline_ts + 50000 < now_ts) { \ - ph_g_usb_##x_dev##_online = false; /* Долгий оффлайн найден */ \ - } \ - prev_online = online; - - if (_kbd_iface >= 0) { - CHECK_IFACE(kbd); - _kbd_sync_report(force); - } - - if (_mouse_iface >= 0) { - CHECK_IFACE(mouse); - (void)force; - } - -# undef CHECK_IFACE - next_ts = time_us_64() + 1000; // Every 1 ms - } - } -} - -void ph_usb_kbd_send_key(u8 key, bool state) { - if (_kbd_iface < 0) { - return; // Допускаем планирование нажатия, пока устройство не готово - } - - if (key >= HID_KEY_CONTROL_LEFT && key <= HID_KEY_GUI_RIGHT) { // 0xE0...0xE7 - Modifiers - key = 1 << (key & 0x07); // Номер означает сдвиг - if (state) { - _kbd_mods |= key; - } else { - _kbd_mods &= ~key; - } - - } else { // Regular keys - if (state) { - s8 pos = -1; - for (u8 i = 0; i < 6; ++i) { - if (_kbd_keys[i] == key) { - goto already_pressed; - } else if (_kbd_keys[i] == 0) { - pos = i; - } - } - _kbd_keys[pos >= 0 ? pos : 0] = key; - // already_pressed: - } else { - for (u8 i = 0; i < 6; ++i) { - if (_kbd_keys[i] == key) { - _kbd_keys[i] = 0; - break; - } - } - } - } - already_pressed: // Old GCC doesn't like ^ that label in the end of block - - _kbd_sync_report(true); -} - -void ph_usb_mouse_send_button(u8 button, bool state) { - if (!PH_O_IS_MOUSE_USB) { - return; - } - if (state) { - _mouse_buttons |= button; - } else { - _mouse_buttons &= ~button; - } - if (PH_O_IS_MOUSE_USB_ABS) { - _mouse_abs_send_report(0, 0); - } else { // PH_O_IS_MOUSE_USB_REL - _mouse_rel_send_report(0, 0, 0, 0); - } -} - -void ph_usb_mouse_send_abs(s16 x, s16 y) { - if (PH_O_IS_MOUSE_USB_ABS) { - _mouse_abs_x = x; - _mouse_abs_y = y; - _mouse_abs_send_report(0, 0); - } -} - -void ph_usb_mouse_send_rel(s8 x, s8 y) { - if (PH_O_IS_MOUSE_USB_REL) { - _mouse_rel_send_report(x, y, 0, 0); - } -} - -void ph_usb_mouse_send_wheel(s8 h, s8 v) { - if (PH_O_IS_MOUSE_USB_ABS) { - _mouse_abs_send_report(h, v); - } else { // PH_O_IS_MOUSE_USB_REL - _mouse_rel_send_report(0, 0, h, v); - } -} - -void ph_usb_send_clear(void) { - if (PH_O_IS_KBD_USB) { - _KBD_CLEAR; - _kbd_sync_report(true); - } - if (PH_O_IS_MOUSE_USB) { - _MOUSE_CLEAR; - if (PH_O_IS_MOUSE_USB_ABS) { - _mouse_abs_send_report(0, 0); - } else { // PH_O_IS_MOUSE_USB_REL - _mouse_rel_send_report(0, 0, 0, 0); - } - } -} - -//-------------------------------------------------------------------- -// RAW report senders -//-------------------------------------------------------------------- - -static void _kbd_sync_report(bool new) { - static bool sent = true; - if (_kbd_iface < 0 || !PH_O_IS_KBD_USB) { - _KBD_CLEAR; - sent = true; - return; - } - if (new) { - sent = false; - } - if (!sent) { - if (tud_suspended()) { - tud_remote_wakeup(); - //_KBD_CLEAR; - //sent = true; - } else { - sent = tud_hid_n_keyboard_report(_kbd_iface, 0, _kbd_mods, _kbd_keys); - } - } -} - -#define _CHECK_MOUSE(x_mode) { \ - if (_mouse_iface < 0 || !PH_O_IS_MOUSE_USB_##x_mode) { _MOUSE_CLEAR; return; } \ - if (tud_suspended()) { tud_remote_wakeup(); _MOUSE_CLEAR; return; } \ - } - - -static void _mouse_abs_send_report(s8 h, s8 v) { - (void)h; // Horizontal scrolling is not supported due BIOS/UEFI compatibility reasons - _CHECK_MOUSE(ABS); - u16 x = ((s32)_mouse_abs_x + 32768) / 2; - u16 y = ((s32)_mouse_abs_y + 32768) / 2; - if (PH_O_MOUSE(USB_W98)) { - x <<= 1; - y <<= 1; - } - struct TU_ATTR_PACKED { - u8 buttons; - u16 x; - u16 y; - s8 v; - } report = {_mouse_buttons, x, y, v}; - tud_hid_n_report(_mouse_iface, 0, &report, sizeof(report)); -} - -static void _mouse_rel_send_report(s8 x, s8 y, s8 h, s8 v) { - (void)h; // Horizontal scrolling is not supported due BIOS/UEFI compatibility reasons - _CHECK_MOUSE(REL); - struct TU_ATTR_PACKED { - u8 buttons; - s8 x; - s8 y; - s8 v; - } report = {_mouse_buttons, x, y, v}; - tud_hid_n_report(_mouse_iface, 0, &report, sizeof(report)); -} - -#undef _CHECK_MOUSE - - -//-------------------------------------------------------------------- -// Device callbacks -//-------------------------------------------------------------------- - -u16 tud_hid_get_report_cb(u8 iface, u8 report_id, hid_report_type_t report_type, u8 *buf, u16 len) { - // Invoked when received GET_REPORT control request, return 0 == STALL - (void)iface; - (void)report_id; - (void)report_type; - (void)buf; - (void)len; - return 0; -} - -void tud_hid_set_report_cb(u8 iface, u8 report_id, hid_report_type_t report_type, const u8 *buf, u16 len) { - // Invoked when received SET_REPORT control request - // or received data on OUT endpoint (ReportID=0, Type=0) - (void)report_id; - if (iface == _kbd_iface && report_type == HID_REPORT_TYPE_OUTPUT && len >= 1) { - ph_g_usb_kbd_leds = buf[0]; - } -} - -const u8 *tud_hid_descriptor_report_cb(u8 iface) { - if ((int)iface == _mouse_iface) { - if (PH_O_IS_MOUSE_USB_ABS) { - return PH_USB_MOUSE_ABS_DESC; - } else { // PH_O_IS_MOUSE_USB_REL - return PH_USB_MOUSE_REL_DESC; - } - } - return PH_USB_KBD_DESC; // _kbd_iface, PH_O_IS_KBD_USB -} - -const u8 *_bridge_tud_descriptor_configuration_cb(void) { - enum {num_cdc = 0, num_cdc_data, num_total}; - static const u8 desc[] = { - TUD_CONFIG_DESCRIPTOR( - 1, // Config number - num_total,// Interface count - 0, // String index - (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN), // Total length - 0, // Attribute - 100 // Power in mA - ), - TUD_CDC_DESCRIPTOR( - num_cdc,// Interface number - 4, // String index - 0x81, // EPNUM_CDC_NOTIF - EP notification address - 8, // EP notification size - 0x02, // EPNUM_CDC_OUT - EP OUT data address - 0x82, // EPNUM_CDC_IN - EP IN data address - 64 // EP size - ), - }; - return desc; -} - -const u8 *_hid_tud_descriptor_configuration_cb(void) { - static u8 desc[TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN * 2] = {0}; - static bool filled = false; - - if (!filled) { - uz offset = TUD_CONFIG_DESC_LEN; - u8 iface = 0; - u8 ep = 0x81; - -# define APPEND_DESC(x_proto, x_desc, x_iface_to) { \ - const u8 part[] = {TUD_HID_DESCRIPTOR( \ - (x_iface_to = iface), /* Interface number */ \ - 0, x_proto, x_desc##_LEN, /* String index, protocol, report descriptor len */ \ - ep, CFG_TUD_HID_EP_BUFSIZE, 1)}; /* EP In address, size, polling interval */ \ - memcpy(desc + offset, part, TUD_HID_DESC_LEN); \ - offset += TUD_HID_DESC_LEN; ++iface; ++ep; \ - } - - if (PH_O_IS_KBD_USB) { - APPEND_DESC(HID_ITF_PROTOCOL_KEYBOARD, PH_USB_KBD_DESC, _kbd_iface); - } - if (PH_O_IS_MOUSE_USB_ABS) { - APPEND_DESC(HID_ITF_PROTOCOL_NONE, PH_USB_MOUSE_ABS_DESC, _mouse_iface); - } else if (PH_O_IS_MOUSE_USB_REL) { - APPEND_DESC(HID_ITF_PROTOCOL_MOUSE, PH_USB_MOUSE_REL_DESC, _mouse_iface); - } - -# undef APPEND_DESC - - // Config number, interface count, string index, total length, attribute, power in mA - const u8 part[] = {TUD_CONFIG_DESCRIPTOR(1, iface, 0, offset, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100)}; - memcpy(desc, part, TUD_CONFIG_DESC_LEN); - filled = true; - } - return desc; -} - -const u8 *tud_descriptor_configuration_cb(u8 index) { - // Invoked when received GET CONFIGURATION DESCRIPTOR - (void)index; - if (ph_g_is_bridge) { - return _bridge_tud_descriptor_configuration_cb(); - } - return _hid_tud_descriptor_configuration_cb(); -} - -const u8 *tud_descriptor_device_cb(void) { - // Invoked when received GET DEVICE DESCRIPTOR - static tusb_desc_device_t desc = { - .bLength = sizeof(tusb_desc_device_t), - .bDescriptorType = TUSB_DESC_DEVICE, - .bcdUSB = 0x0200, - - .bDeviceClass = 0, - .bDeviceSubClass = 0, - .bDeviceProtocol = 0, - - .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, - - .idVendor = 0x1209, // https://pid.codes/org/Pi-KVM - .idProduct = 0xEDA2, - .bcdDevice = 0x0100, - - .iManufacturer = 1, - .iProduct = 2, - .iSerialNumber = 3, - - .bNumConfigurations = 1, - }; - if (ph_g_is_bridge) { - desc.bDeviceClass = TUSB_CLASS_MISC; - desc.bDeviceSubClass = MISC_SUBCLASS_COMMON; - desc.bDeviceProtocol = MISC_PROTOCOL_IAD; - desc.idProduct = 0xEDA3; - } - return (const u8 *)&desc; -} - -const u16 *tud_descriptor_string_cb(u8 index, u16 lang_id) { - // Invoked when received GET STRING DESCRIPTOR request. - (void)lang_id; - - static u16 desc_str[32]; - uz desc_str_len; - - if (index == 0) { - desc_str[1] = 0x0409; // Supported language is English (0x0409) - desc_str_len = 1; - } else { - char str[32]; - switch (index) { - case 1: strcpy(str, "PiKVM"); break; // Manufacturer - case 2: strcpy(str, (ph_g_is_bridge ? "PiKVM HID Bridge" : "PiKVM HID")); break; // Product - case 3: pico_get_unique_board_id_string(str, 32); break; // Serial - case 4: { - if (ph_g_is_bridge) { - strcpy(str, "PiKVM HID Bridge CDC"); - } else { - return NULL; - } - }; break; - default: return NULL; - } - desc_str_len = strlen(str); - for (uz i = 0; i < desc_str_len; ++i) { - desc_str[i + 1] = str[i]; // Convert ASCII string into UTF-16 - } - } - - // First byte is length (including header), second byte is string type - desc_str[0] = (TUSB_DESC_STRING << 8) | (2 * desc_str_len + 2); - return desc_str; -} diff --git a/hid/pico/src/ph_usb.h b/hid/pico/src/ph_usb.h deleted file mode 100644 index 357f578d..00000000 --- a/hid/pico/src/ph_usb.h +++ /dev/null @@ -1,43 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#pragma once - -#include "ph_types.h" - - -extern u8 ph_g_usb_kbd_leds; -extern bool ph_g_usb_kbd_online; -extern bool ph_g_usb_mouse_online; - - -void ph_usb_init(void); -void ph_usb_task(void); - -void ph_usb_kbd_send_key(u8 key, bool state); - -void ph_usb_mouse_send_button(u8 button, bool state); -void ph_usb_mouse_send_abs(s16 x, s16 y); -void ph_usb_mouse_send_rel(s8 x, s8 y); -void ph_usb_mouse_send_wheel(s8 h, s8 v); - -void ph_usb_send_clear(void); diff --git a/hid/pico/src/ph_usb_kbd.c b/hid/pico/src/ph_usb_kbd.c deleted file mode 100644 index b4613c27..00000000 --- a/hid/pico/src/ph_usb_kbd.c +++ /dev/null @@ -1,78 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#include "ph_usb_kbd.h" - -#include "ph_types.h" - - -const u8 PH_USB_KBD_DESC[] = { - // Logitech descriptor. It's very similar to https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt - // Dumped using usbhid-dump; parsed using https://eleccelerator.com/usbdescreqparser - - // Keyboard - 0x05, 0x01, // USAGE_PAGE (Generic Desktop) - 0x09, 0x06, // USAGE (Keyboard) - 0xA1, 0x01, // COLLECTION (Application) - - // Modifiers - 0x05, 0x07, // USAGE_PAGE (Keyboard) - 0x19, 0xE0, // USAGE_MINIMUM (Keyboard LeftControl) - 0x29, 0xE7, // USAGE_MAXIMUM (Keyboard Right GUI) - 0x15, 0x00, // LOGICAL_MINIMUM (0) - 0x25, 0x01, // LOGICAL_MAXIMUM (1) - 0x75, 0x01, // REPORT_SIZE (1) - 0x95, 0x08, // REPORT_COUNT (8) - 0x81, 0x02, // INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) - - // Reserved byte - 0x95, 0x01, // REPORT_COUNT (1) - 0x75, 0x08, // REPORT_SIZE (8) - 0x81, 0x01, // INPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) - - // LEDs output - 0x95, 0x05, // REPORT_COUNT (5) - 0x75, 0x01, // REPORT_SIZE (1) - 0x05, 0x08, // USAGE_PAGE (LEDs) - 0x19, 0x01, // USAGE_MINIMUM (Num Lock) - 0x29, 0x05, // USAGE_MAXIMUM (Kana) - 0x91, 0x02, // OUTPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) - - // Reserved 3 bits in output - 0x95, 0x01, // REPORT_COUNT (1) - 0x75, 0x03, // REPORT_SIZE (3) - 0x91, 0x01, // OUTPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) - - // 6 keys - 0x95, 0x06, // REPORT_COUNT (6) - 0x75, 0x08, // REPORT_SIZE (8) - 0x15, 0x00, // LOGICAL_MINIMUM (0) - 0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (0xFF) - 0x05, 0x07, // USAGE_PAGE (Keyboard) - 0x19, 0x00, // USAGE_MINIMUM (Reserved) - 0x2A, 0xFF, 0x00, // USAGE_MAXIMUM (0xFF) - 0x81, 0x00, // INPUT (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) - - 0xC0, // END_COLLECTION -}; - -const uz PH_USB_KBD_DESC_LEN = sizeof(PH_USB_KBD_DESC); diff --git a/hid/pico/src/ph_usb_kbd.h b/hid/pico/src/ph_usb_kbd.h deleted file mode 100644 index 8bbdcd05..00000000 --- a/hid/pico/src/ph_usb_kbd.h +++ /dev/null @@ -1,29 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#pragma once - -#include "ph_types.h" - - -extern const u8 PH_USB_KBD_DESC[]; -extern const uz PH_USB_KBD_DESC_LEN; diff --git a/hid/pico/src/ph_usb_keymap.h b/hid/pico/src/ph_usb_keymap.h deleted file mode 100644 index f2481e41..00000000 --- a/hid/pico/src/ph_usb_keymap.h +++ /dev/null @@ -1,143 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "ph_types.h" - - -inline u8 ph_usb_keymap(u8 key) { - switch (key) { - case 1: return 4; // KeyA - case 2: return 5; // KeyB - case 3: return 6; // KeyC - case 4: return 7; // KeyD - case 5: return 8; // KeyE - case 6: return 9; // KeyF - case 7: return 10; // KeyG - case 8: return 11; // KeyH - case 9: return 12; // KeyI - case 10: return 13; // KeyJ - case 11: return 14; // KeyK - case 12: return 15; // KeyL - case 13: return 16; // KeyM - case 14: return 17; // KeyN - case 15: return 18; // KeyO - case 16: return 19; // KeyP - case 17: return 20; // KeyQ - case 18: return 21; // KeyR - case 19: return 22; // KeyS - case 20: return 23; // KeyT - case 21: return 24; // KeyU - case 22: return 25; // KeyV - case 23: return 26; // KeyW - case 24: return 27; // KeyX - case 25: return 28; // KeyY - case 26: return 29; // KeyZ - case 27: return 30; // Digit1 - case 28: return 31; // Digit2 - case 29: return 32; // Digit3 - case 30: return 33; // Digit4 - case 31: return 34; // Digit5 - case 32: return 35; // Digit6 - case 33: return 36; // Digit7 - case 34: return 37; // Digit8 - case 35: return 38; // Digit9 - case 36: return 39; // Digit0 - case 37: return 40; // Enter - case 38: return 41; // Escape - case 39: return 42; // Backspace - case 40: return 43; // Tab - case 41: return 44; // Space - case 42: return 45; // Minus - case 43: return 46; // Equal - case 44: return 47; // BracketLeft - case 45: return 48; // BracketRight - case 46: return 49; // Backslash - case 47: return 51; // Semicolon - case 48: return 52; // Quote - case 49: return 53; // Backquote - case 50: return 54; // Comma - case 51: return 55; // Period - case 52: return 56; // Slash - case 53: return 57; // CapsLock - case 54: return 58; // F1 - case 55: return 59; // F2 - case 56: return 60; // F3 - case 57: return 61; // F4 - case 58: return 62; // F5 - case 59: return 63; // F6 - case 60: return 64; // F7 - case 61: return 65; // F8 - case 62: return 66; // F9 - case 63: return 67; // F10 - case 64: return 68; // F11 - case 65: return 69; // F12 - case 66: return 70; // PrintScreen - case 67: return 73; // Insert - case 68: return 74; // Home - case 69: return 75; // PageUp - case 70: return 76; // Delete - case 71: return 77; // End - case 72: return 78; // PageDown - case 73: return 79; // ArrowRight - case 74: return 80; // ArrowLeft - case 75: return 81; // ArrowDown - case 76: return 82; // ArrowUp - case 77: return 224; // ControlLeft - case 78: return 225; // ShiftLeft - case 79: return 226; // AltLeft - case 80: return 227; // MetaLeft - case 81: return 228; // ControlRight - case 82: return 229; // ShiftRight - case 83: return 230; // AltRight - case 84: return 231; // MetaRight - case 85: return 72; // Pause - case 86: return 71; // ScrollLock - case 87: return 83; // NumLock - case 88: return 101; // ContextMenu - case 89: return 84; // NumpadDivide - case 90: return 85; // NumpadMultiply - case 91: return 86; // NumpadSubtract - case 92: return 87; // NumpadAdd - case 93: return 88; // NumpadEnter - case 94: return 89; // Numpad1 - case 95: return 90; // Numpad2 - case 96: return 91; // Numpad3 - case 97: return 92; // Numpad4 - case 98: return 93; // Numpad5 - case 99: return 94; // Numpad6 - case 100: return 95; // Numpad7 - case 101: return 96; // Numpad8 - case 102: return 97; // Numpad9 - case 103: return 98; // Numpad0 - case 104: return 99; // NumpadDecimal - case 105: return 102; // Power - case 106: return 100; // IntlBackslash - case 107: return 137; // IntlYen - case 108: return 135; // IntlRo - case 109: return 136; // KanaMode - case 110: return 138; // Convert - case 111: return 139; // NonConvert - } - return 0; -} diff --git a/hid/pico/src/ph_usb_keymap.h.mako b/hid/pico/src/ph_usb_keymap.h.mako deleted file mode 100644 index 454f9c1e..00000000 --- a/hid/pico/src/ph_usb_keymap.h.mako +++ /dev/null @@ -1,39 +0,0 @@ -/***************************************************************************** -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -*****************************************************************************/ - - -#pragma once - -#include "ph_types.h" - -<%! import operator %> -inline u8 ph_usb_keymap(u8 key) { - switch (key) { -% for km in sorted(keymap, key=operator.attrgetter("mcu_code")): - % if km.usb_key.is_modifier: - case ${km.mcu_code}: return ${km.usb_key.arduino_modifier_code}; // ${km.web_name} - % else: - case ${km.mcu_code}: return ${km.usb_key.code}; // ${km.web_name} - % endif -% endfor - } - return 0; -} diff --git a/hid/pico/src/ph_usb_mouse.c b/hid/pico/src/ph_usb_mouse.c deleted file mode 100644 index 33f35410..00000000 --- a/hid/pico/src/ph_usb_mouse.c +++ /dev/null @@ -1,120 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#include "ph_usb_mouse.h" - -#include "ph_types.h" - - -const u8 PH_USB_MOUSE_ABS_DESC[] = { - // https://github.com/NicoHood/HID/blob/0835e6a/src/SingleReport/SingleAbsoluteMouse.cpp - // Репорт взят отсюда ^^^, но изменен диапазон значений координат перемещений. - // Автор предлагает использовать -32768...32767, но семерка почему-то не хочет работать - // с отрицательными значениями координат, как не хочет хавать 65536 и 32768. - // Так что мы ей скармливаем диапазон 0...32767, и передаем рукожопам из микрософта привет, - // потому что линуксы прекрасно работают с любыми двухбайтовыми диапазонами. - - // Absolute mouse - 0x05, 0x01, // USAGE_PAGE (Generic Desktop) - 0x09, 0x02, // USAGE (Mouse) - 0xA1, 0x01, // COLLECTION (Application) - - // Pointer and Physical are required by Apple Recovery - 0x09, 0x01, // USAGE (Pointer) - 0xA1, 0x00, // COLLECTION (Physical) - - // 8 Buttons - 0x05, 0x09, // USAGE_PAGE (Button) - 0x19, 0x01, // USAGE_MINIMUM (Button 1) - 0x29, 0x08, // USAGE_MAXIMUM (Button 8) - 0x15, 0x00, // LOGICAL_MINIMUM (0) - 0x25, 0x01, // LOGICAL_MAXIMUM (1) - 0x95, 0x08, // REPORT_COUNT (8) - 0x75, 0x01, // REPORT_SIZE (1) - 0x81, 0x02, // INPUT (Data,Var,Abs) - - // X, Y - 0x05, 0x01, // USAGE_PAGE (Generic Desktop) - 0x09, 0x30, // USAGE (X) - 0x09, 0x31, // USAGE (Y) - 0x16, 0x00, 0x00, // LOGICAL_MINIMUM (0) - 0x26, 0xFF, 0x7F, // LOGICAL_MAXIMUM (32767) - 0x75, 0x10, // REPORT_SIZE (16) - 0x95, 0x02, // REPORT_COUNT (2) - 0x81, 0x02, // INPUT (Data,Var,Abs) - - // Wheel - 0x09, 0x38, // USAGE (Wheel) - 0x15, 0x81, // LOGICAL_MINIMUM (-127) - 0x25, 0x7F, // LOGICAL_MAXIMUM (127) - 0x75, 0x08, // REPORT_SIZE (8) - 0x95, 0x01, // REPORT_COUNT (1) - 0x81, 0x06, // INPUT (Data,Var,Rel) - - // End - 0xC0, // END_COLLECTION (Physical) - 0xC0, // END_COLLECTION -}; - -const uz PH_USB_MOUSE_ABS_DESC_LEN = sizeof(PH_USB_MOUSE_ABS_DESC); - -const u8 PH_USB_MOUSE_REL_DESC[] = { - // https://github.com/NicoHood/HID/blob/0835e6a/src/SingleReport/BootMouse.cpp - - // Relative mouse - 0x05, 0x01, // USAGE_PAGE (Generic Desktop) - 0x09, 0x02, // USAGE (Mouse) - 0xA1, 0x01, // COLLECTION (Application) - - // Pointer and Physical are required by Apple Recovery - 0x09, 0x01, // USAGE (Pointer) - 0xA1, 0x00, // COLLECTION (Physical) - - // 8 Buttons - 0x05, 0x09, // USAGE_PAGE (Button) - 0x19, 0x01, // USAGE_MINIMUM (Button 1) - 0x29, 0x08, // USAGE_MAXIMUM (Button 8) - 0x15, 0x00, // LOGICAL_MINIMUM (0) - 0x25, 0x01, // LOGICAL_MAXIMUM (1) - 0x95, 0x08, // REPORT_COUNT (8) - 0x75, 0x01, // REPORT_SIZE (1) - 0x81, 0x02, // INPUT (Data,Var,Abs) - - // X, Y - 0x05, 0x01, // USAGE_PAGE (Generic Desktop) - 0x09, 0x30, // USAGE (X) - 0x09, 0x31, // USAGE (Y) - - // Wheel - 0x09, 0x38, // USAGE (Wheel) - 0x15, 0x81, // LOGICAL_MINIMUM (-127) - 0x25, 0x7F, // LOGICAL_MAXIMUM (127) - 0x75, 0x08, // REPORT_SIZE (8) - 0x95, 0x03, // REPORT_COUNT (3) - 0x81, 0x06, // INPUT (Data,Var,Rel) - - // End - 0xC0, // END_COLLECTION (Physical) - 0xC0, // END_COLLECTION -}; - -const uz PH_USB_MOUSE_REL_DESC_LEN = sizeof(PH_USB_MOUSE_REL_DESC); diff --git a/hid/pico/src/ph_usb_mouse.h b/hid/pico/src/ph_usb_mouse.h deleted file mode 100644 index 1d13d73d..00000000 --- a/hid/pico/src/ph_usb_mouse.h +++ /dev/null @@ -1,32 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#pragma once - -#include "ph_types.h" - - -extern const u8 PH_USB_MOUSE_ABS_DESC[]; -extern const uz PH_USB_MOUSE_ABS_DESC_LEN; - -extern const u8 PH_USB_MOUSE_REL_DESC[]; -extern const uz PH_USB_MOUSE_REL_DESC_LEN; diff --git a/hid/pico/src/tusb_config.h b/hid/pico/src/tusb_config.h deleted file mode 100644 index a3986ee1..00000000 --- a/hid/pico/src/tusb_config.h +++ /dev/null @@ -1,79 +0,0 @@ -/* ========================================================================= # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================= */ - - -#pragma once - - -//-------------------------------------------------------------------- -// Common config -//-------------------------------------------------------------------- - -//#define CFG_TUSB_DEBUG 100 - -#define CFG_TUSB_OS OPT_OS_PICO - -// Enable device stack -#define CFG_TUD_ENABLED 1 - -// CFG_TUSB_DEBUG is defined by compiler in DEBUG build -//#define CFG_TUSB_DEBUG 100 - -// USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. -// Tinyusb use follows macros to declare transferring memory so that they can be put -// into those specific section. -// - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) -// - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) -#ifndef CFG_TUSB_MEM_SECTION -# define CFG_TUSB_MEM_SECTION -#endif - -#ifndef CFG_TUSB_MEM_ALIGN -# define CFG_TUSB_MEM_ALIGN __attribute__((aligned(4))) -#endif - - -//-------------------------------------------------------------------- -// Device config -//-------------------------------------------------------------------- - -#ifndef CFG_TUD_ENDPOINT0_SIZE -# define CFG_TUD_ENDPOINT0_SIZE 64 -#endif - -// HID: Keyboard + Mouse -#define CFG_TUD_HID 2 - -// HID buffer size Should be sufficient to hold ID (if any) + Data -#ifndef CFG_TUD_HID_EP_BUFSIZE -# define CFG_TUD_HID_EP_BUFSIZE 16 -#endif - - -// CDC for the bridge mode -#define CFG_TUD_CDC 1 - -// CDC FIFO size of TX and RX -#define CFG_TUD_CDC_RX_BUFSIZE 4096 -#define CFG_TUD_CDC_TX_BUFSIZE 4096 - -// CDC Endpoint transfer buffer size, more is faster -#define CFG_TUD_CDC_EP_BUFSIZE 64 diff --git a/kvmd/apps/kvmd/api/auth.py b/kvmd/apps/kvmd/api/auth.py index dee4a85d..28d56131 100644 --- a/kvmd/apps/kvmd/api/auth.py +++ b/kvmd/apps/kvmd/api/auth.py @@ -84,7 +84,7 @@ class AuthApi: # ===== - @exposed_http("POST", "/auth/login", auth_required=False) + @exposed_http("POST", "/api/auth/login", auth_required=False) async def __login_handler(self, req: Request) -> Response: if self.__auth_manager.is_auth_enabled(): credentials = await req.post() @@ -97,13 +97,13 @@ class AuthApi: raise ForbiddenError() return make_json_response() - @exposed_http("POST", "/auth/logout") + @exposed_http("POST", "/api/auth/logout") async def __logout_handler(self, req: Request) -> Response: if self.__auth_manager.is_auth_enabled(): token = valid_auth_token(req.cookies.get(_COOKIE_AUTH_TOKEN, "")) self.__auth_manager.logout(token) return make_json_response() - @exposed_http("GET", "/auth/check") + @exposed_http("GET", "/api/auth/check") async def __check_handler(self, _: Request) -> Response: return make_json_response() diff --git a/kvmd/apps/kvmd/api/export.py b/kvmd/apps/kvmd/api/export.py index bb048f53..23bcfd4d 100644 --- a/kvmd/apps/kvmd/api/export.py +++ b/kvmd/apps/kvmd/api/export.py @@ -49,7 +49,7 @@ class ExportApi: # ===== - @exposed_http("GET", "/export/prometheus/metrics") + @exposed_http("GET", "/api/export/prometheus/metrics") async def __prometheus_metrics_handler(self, _: Request) -> Response: return Response(text=(await self.__get_prometheus_metrics())) diff --git a/kvmd/apps/kvmd/api/hid.py b/kvmd/apps/kvmd/api/hid.py index 51b9dc00..45f619e4 100644 --- a/kvmd/apps/kvmd/api/hid.py +++ b/kvmd/apps/kvmd/api/hid.py @@ -73,7 +73,7 @@ class HidApi: async def __state_handler(self, _: Request) -> Response: return make_json_response(await self.__hid.get_state()) - @exposed_http("POST", "/hid/set_params") + @exposed_http("POST", "/api/hid/set_params") async def __set_params_handler(self, req: Request) -> Response: params = { key: validator(req.query.get(key)) @@ -87,12 +87,12 @@ class HidApi: self.__hid.set_params(**params) # type: ignore return make_json_response() - @exposed_http("POST", "/hid/set_connected") + @exposed_http("POST", "/api/hid/set_connected") async def __set_connected_handler(self, req: Request) -> Response: self.__hid.set_connected(valid_bool(req.query.get("connected"))) return make_json_response() - @exposed_http("POST", "/hid/reset") + @exposed_http("POST", "/api/hid/reset") async def __reset_handler(self, _: Request) -> Response: await self.__hid.reset() return make_json_response() @@ -112,11 +112,11 @@ class HidApi: }, } - @exposed_http("GET", "/hid/keymaps") + @exposed_http("GET", "/api/hid/keymaps") async def __keymaps_handler(self, _: Request) -> Response: return make_json_response(await self.get_keymaps()) - @exposed_http("POST", "/hid/print") + @exposed_http("POST", "/api/hid/print") async def __print_handler(self, req: Request) -> Response: text = await req.text() limit = int(valid_int_f0(req.query.get("limit", 1024))) @@ -243,7 +243,7 @@ class HidApi: # ===== - @exposed_http("POST", "/hid/events/send_key") + @exposed_http("POST", "/api/hid/events/send_key") async def __events_send_key_handler(self, req: Request) -> Response: key = valid_hid_key(req.query.get("key")) if "state" in req.query: @@ -253,7 +253,7 @@ class HidApi: self.__hid.send_key_events([(key, True), (key, False)]) return make_json_response() - @exposed_http("POST", "/hid/events/send_mouse_button") + @exposed_http("POST", "/api/hid/events/send_mouse_button") async def __events_send_mouse_button_handler(self, req: Request) -> Response: button = valid_hid_mouse_button(req.query.get("button")) if "state" in req.query: @@ -264,18 +264,18 @@ class HidApi: self.__hid.send_mouse_button_event(button, False) return make_json_response() - @exposed_http("POST", "/hid/events/send_mouse_move") + @exposed_http("POST", "/api/hid/events/send_mouse_move") async def __events_send_mouse_move_handler(self, req: Request) -> Response: to_x = valid_hid_mouse_move(req.query.get("to_x")) to_y = valid_hid_mouse_move(req.query.get("to_y")) self.__hid.send_mouse_move_event(to_x, to_y) return make_json_response() - @exposed_http("POST", "/hid/events/send_mouse_relative") + @exposed_http("POST", "/api/hid/events/send_mouse_relative") async def __events_send_mouse_relative_handler(self, req: Request) -> Response: return self.__process_http_delta_event(req, self.__hid.send_mouse_relative_event) - @exposed_http("POST", "/hid/events/send_mouse_wheel") + @exposed_http("POST", "/api/hid/events/send_mouse_wheel") async def __events_send_mouse_wheel_handler(self, req: Request) -> Response: return self.__process_http_delta_event(req, self.__hid.send_mouse_wheel_event) diff --git a/kvmd/apps/kvmd/api/info.py b/kvmd/apps/kvmd/api/info.py index 89d45a84..f2badaee 100644 --- a/kvmd/apps/kvmd/api/info.py +++ b/kvmd/apps/kvmd/api/info.py @@ -20,8 +20,18 @@ # ========================================================================== # +import os +from aiohttp import ClientConnectionError, ClientSession +from aiohttp import UnixConnector from aiohttp.web import Request from aiohttp.web import Response +from aiohttp.web import HTTPForbidden +from aiohttp.web import HTTPNotFound +from aiohttp.web import FileResponse +from aiohttp.web import HTTPInternalServerError + +from aiohttp.web import StreamResponse +from urllib.parse import urlparse, urlunparse, parse_qs, urlencode from ....htserver import exposed_http from ....htserver import make_json_response @@ -35,10 +45,13 @@ from ..info import InfoManager class InfoApi: def __init__(self, info_manager: InfoManager) -> None: self.__info_manager = info_manager + self.static_dir = 'kvmd_data/usr/share/kvmd/web' + self.target_stream_server = 'http://127.0.0.1:8081' # ===== - @exposed_http("GET", "/info") + + @exposed_http("GET", "/api/info") async def __common_state_handler(self, req: Request) -> Response: fields = self.__valid_info_fields(req) return make_json_response(await self.__info_manager.get_state(fields)) @@ -49,3 +62,52 @@ class InfoApi: arg=req.query.get("fields", ",".join(available)), variants=available, ) or available) + + @exposed_http("GET", "/streamer/stream") + async def proxy_stream_handler(self, request): + socket_path = '/home/mofeng/One-KVM/kvmd_data/run/kvmd/ustreamer.sock' + query_string = urlencode(request.query) + headers = request.headers.copy() + try: + async with ClientSession(connector=UnixConnector(path=socket_path)) as session: + backend_url = f'http://localhost/stream?{query_string}' if query_string else 'http://localhost/stream' + async with session.get(backend_url, headers=headers) as resp: + response = StreamResponse(status=resp.status, reason=resp.reason, headers=resp.headers) + await response.prepare(request) + while True: + chunk = await resp.content.read(512000) + if not chunk: + break + await response.write(chunk) + return response + except ClientConnectionError: + return Response(status=500, text="Client connection was closed") + + + @exposed_http("GET", "/{path:.*}", auth_required=False) + async def __html_file_handler(self, req: Request) -> Response: + path = req.match_info['path'] + full_path = os.path.normpath(os.path.join(self.static_dir, path)) + print("---------------") + print(full_path) + + # 安全检查:确保请求的文件在允许的基础目录内 + if not full_path.startswith(self.static_dir): + raise HTTPForbidden(text="Access denied.") + + if os.path.isdir(full_path): + index_path = os.path.join(full_path, 'index.html') + if os.path.isfile(index_path): + full_path = index_path + else: + raise HTTPNotFound(text="Directory does not contain an index.html file.") + + # 检查调整后的路径是否为现有文件 + if not (os.path.exists(full_path) and os.path.isfile(full_path)): + raise HTTPNotFound(text="File not found.") + + try: + return FileResponse(full_path) + except IOError as e: + raise HTTPInternalServerError(text=str(e)) + \ No newline at end of file diff --git a/kvmd/apps/kvmd/api/log.py b/kvmd/apps/kvmd/api/log.py index 1d31ae8f..74af8637 100644 --- a/kvmd/apps/kvmd/api/log.py +++ b/kvmd/apps/kvmd/api/log.py @@ -46,7 +46,7 @@ class LogApi: # ===== - @exposed_http("GET", "/log") + @exposed_http("GET", "/api/log") async def __log_handler(self, req: Request) -> StreamResponse: if self.__log_reader is None: raise LogReaderDisabledError() diff --git a/kvmd/apps/kvmd/api/msd.py b/kvmd/apps/kvmd/api/msd.py index ca1c6cf1..462490b3 100644 --- a/kvmd/apps/kvmd/api/msd.py +++ b/kvmd/apps/kvmd/api/msd.py @@ -61,7 +61,7 @@ class MsdApi: # ===== - @exposed_http("GET", "/msd") + @exposed_http("GET", "/api/msd") async def __state_handler(self, _: Request) -> Response: state = await self.__msd.get_state() if state["storage"] and state["storage"]["parts"]: @@ -69,7 +69,7 @@ class MsdApi: state["storage"]["free"] = state["storage"]["parts"][""]["free"] # Legacy API return make_json_response(state) - @exposed_http("POST", "/msd/set_params") + @exposed_http("POST", "/api/msd/set_params") async def __set_params_handler(self, req: Request) -> Response: params = { key: validator(req.query.get(param)) @@ -83,19 +83,19 @@ class MsdApi: await self.__msd.set_params(**params) # type: ignore return make_json_response() - @exposed_http("POST", "/msd/set_connected") + @exposed_http("POST", "/api/msd/set_connected") async def __set_connected_handler(self, req: Request) -> Response: await self.__msd.set_connected(valid_bool(req.query.get("connected"))) return make_json_response() - @exposed_http("POST", "/msd/make_image") + @exposed_http("POST", "/api/msd/make_image") async def __set_zipped_handler(self, req: Request) -> Response: await self.__msd.make_image(valid_bool(req.query.get("zipped"))) return make_json_response() # ===== - @exposed_http("GET", "/msd/read") + @exposed_http("GET", "/api/msd/read") async def __read_handler(self, req: Request) -> StreamResponse: name = valid_msd_image_name(req.query.get("image")) compressors = { @@ -143,7 +143,7 @@ class MsdApi: # ===== - @exposed_http("POST", "/msd/write") + @exposed_http("POST", "/api/msd/write") async def __write_handler(self, req: Request) -> Response: unsafe_prefix = req.query.get("prefix", "") + "/" name = valid_msd_image_name(unsafe_prefix + req.query.get("image", "")) @@ -159,7 +159,7 @@ class MsdApi: written = await writer.write_chunk(chunk) return make_json_response(self.__make_write_info(name, size, written)) - @exposed_http("POST", "/msd/write_remote") + @exposed_http("POST", "/api/msd/write_remote") async def __write_remote_handler(self, req: Request) -> (Response | StreamResponse): # pylint: disable=too-many-locals unsafe_prefix = req.query.get("prefix", "") + "/" url = valid_url(req.query.get("url")) @@ -223,12 +223,12 @@ class MsdApi: # ===== - @exposed_http("POST", "/msd/remove") + @exposed_http("POST", "/api/msd/remove") async def __remove_handler(self, req: Request) -> Response: await self.__msd.remove(valid_msd_image_name(req.query.get("image"))) return make_json_response() - @exposed_http("POST", "/msd/reset") + @exposed_http("POST", "/api/msd/reset") async def __reset_handler(self, _: Request) -> Response: await self.__msd.reset() return make_json_response() diff --git a/kvmd/apps/kvmd/api/redfish.py b/kvmd/apps/kvmd/api/redfish.py index 3b248685..5943b948 100644 --- a/kvmd/apps/kvmd/api/redfish.py +++ b/kvmd/apps/kvmd/api/redfish.py @@ -65,7 +65,7 @@ class RedfishApi: # ===== - @exposed_http("GET", "/redfish/v1", auth_required=False) + @exposed_http("GET", "/api/redfish/v1", auth_required=False) async def __root_handler(self, _: Request) -> Response: return make_json_response({ "@odata.id": "/redfish/v1", @@ -76,7 +76,7 @@ class RedfishApi: "Systems": {"@odata.id": "/redfish/v1/Systems"}, }, wrap_result=False) - @exposed_http("GET", "/redfish/v1/Systems") + @exposed_http("GET", "/api/redfish/v1/Systems") async def __systems_handler(self, _: Request) -> Response: return make_json_response({ "@odata.id": "/redfish/v1/Systems", @@ -86,7 +86,7 @@ class RedfishApi: "Name": "Computer System Collection", }, wrap_result=False) - @exposed_http("GET", "/redfish/v1/Systems/0") + @exposed_http("GET", "/api/redfish/v1/Systems/0") async def __server_handler(self, _: Request) -> Response: (atx_state, info_state) = await asyncio.gather(*[ self.__atx.get_state(), @@ -110,7 +110,7 @@ class RedfishApi: "PowerState": ("On" if atx_state["leds"]["power"] else "Off"), # type: ignore }, wrap_result=False) - @exposed_http("POST", "/redfish/v1/Systems/0/Actions/ComputerSystem.Reset") + @exposed_http("POST", "/api/redfish/v1/Systems/0/Actions/ComputerSystem.Reset") async def __power_handler(self, req: Request) -> Response: try: action = check_string_in_list( diff --git a/kvmd/apps/kvmd/api/streamer.py b/kvmd/apps/kvmd/api/streamer.py index 81f255d0..ba74755a 100644 --- a/kvmd/apps/kvmd/api/streamer.py +++ b/kvmd/apps/kvmd/api/streamer.py @@ -47,11 +47,11 @@ class StreamerApi: # ===== - @exposed_http("GET", "/streamer") + @exposed_http("GET", "/api/streamer") async def __state_handler(self, _: Request) -> Response: return make_json_response(await self.__streamer.get_state()) - @exposed_http("GET", "/streamer/snapshot") + @exposed_http("GET", "/api/streamer/snapshot") async def __take_snapshot_handler(self, req: Request) -> Response: snapshot = await self.__streamer.take_snapshot( save=valid_bool(req.query.get("save", False)), @@ -92,11 +92,11 @@ class StreamerApi: ) raise UnavailableError() - @exposed_http("DELETE", "/streamer/snapshot") + @exposed_http("DELETE", "/api/streamer/snapshot") async def __remove_snapshot_handler(self, _: Request) -> Response: self.__streamer.remove_snapshot() return make_json_response() - @exposed_http("GET", "/streamer/ocr") + @exposed_http("GET", "/api/streamer/ocr") async def __ocr_handler(self, _: Request) -> Response: return make_json_response({"ocr": (await self.__ocr.get_state())}) diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py index ed85bb24..a3595c6b 100644 --- a/kvmd/apps/kvmd/server.py +++ b/kvmd/apps/kvmd/server.py @@ -197,7 +197,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins # ===== STREAMER CONTROLLER - @exposed_http("POST", "/streamer/set_params") + @exposed_http("POST", "/api/streamer/set_params") async def __streamer_set_params_handler(self, req: Request) -> Response: current_params = self.__streamer.get_params() for (name, validator, exc_cls) in [ @@ -218,7 +218,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins self.__streamer_notifier.notify() return make_json_response() - @exposed_http("POST", "/streamer/reset") + @exposed_http("POST", "/api/streamer/reset") async def __streamer_reset_handler(self, _: Request) -> Response: self.__reset_streamer = True self.__streamer_notifier.notify() @@ -226,7 +226,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins # ===== WEBSOCKET - @exposed_http("GET", "/ws") + @exposed_http("GET", "/api/ws") async def __ws_handler(self, req: Request) -> WebSocketResponse: stream = valid_bool(req.query.get("stream", True)) legacy = valid_bool(req.query.get("legacy", True)) diff --git a/kvmd/clients/__init__.py b/kvmd/clients/__init__.py index e917c9f6..f0645fd2 100644 --- a/kvmd/clients/__init__.py +++ b/kvmd/clients/__init__.py @@ -72,13 +72,23 @@ class BaseHttpClient: def make_session(self) -> BaseHttpClientSession: raise NotImplementedError - def _make_http_session(self, headers: (dict[str, str] | None)=None) -> aiohttp.ClientSession: + def _make_http_session(self, headers: dict[str, str] | None = None) -> aiohttp.ClientSession: + connector = None + #这里临时使用 socket ,后期考虑是否使用 http 方式 + use_unix_socket = True + if use_unix_socket: + connector = aiohttp.UnixConnector(path=self.__unix_path) + base_url = "http://localhost:0" # 继续使用 Unix 域套接字 + else: + base_url = "http://127.0.0.1:8001" # 使用指定的 IP 和端口 + + #print("base_url:", base_url) return aiohttp.ClientSession( - base_url="http://localhost:0", + base_url=base_url, headers={ "User-Agent": self.__user_agent, **(headers or {}), }, - connector=aiohttp.UnixConnector(path=self.__unix_path), + connector=connector, timeout=aiohttp.ClientTimeout(total=self.__timeout), ) diff --git a/kvmd/htserver.py b/kvmd/htserver.py index 351c1328..6b02f36a 100644 --- a/kvmd/htserver.py +++ b/kvmd/htserver.py @@ -291,13 +291,18 @@ class HttpServer: ) -> None: self.__ws_heartbeat = heartbeat - - if unix_rm and os.path.exists(unix_path): - os.remove(unix_path) - server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - server_socket.bind(unix_path) - if unix_mode: - os.chmod(unix_path, unix_mode) + # 默认绑定到所有地址 + host = '0.0.0.0' + port = 8080 + #if unix_rm and os.path.exists(unix_path): + #os.remove(unix_path) + #server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + #server_socket.bind(unix_path) + #if unix_mode: + # os.chmod(unix_path, unix_mode) + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 允许重用地址 + server_socket.bind((host, port)) run_app( sock=server_socket, diff --git a/configs/kvmd/totp.secret b/kvmd_data/etc/kvmd/.docker_flag similarity index 100% rename from configs/kvmd/totp.secret rename to kvmd_data/etc/kvmd/.docker_flag diff --git a/configs/os/cmdline/v1-hdmiusb-rpi2.sed b/kvmd_data/etc/kvmd/.init_flag similarity index 100% rename from configs/os/cmdline/v1-hdmiusb-rpi2.sed rename to kvmd_data/etc/kvmd/.init_flag diff --git a/configs/kvmd/armbain-motd b/kvmd_data/etc/kvmd/armbain-motd similarity index 100% rename from configs/kvmd/armbain-motd rename to kvmd_data/etc/kvmd/armbain-motd diff --git a/configs/kvmd/atx.sh b/kvmd_data/etc/kvmd/atx.sh similarity index 100% rename from configs/kvmd/atx.sh rename to kvmd_data/etc/kvmd/atx.sh diff --git a/configs/kvmd/auth.yaml b/kvmd_data/etc/kvmd/auth.yaml similarity index 100% rename from configs/kvmd/auth.yaml rename to kvmd_data/etc/kvmd/auth.yaml diff --git a/configs/kvmd/clean_when_exit.sh b/kvmd_data/etc/kvmd/clean_when_exit.sh similarity index 100% rename from configs/kvmd/clean_when_exit.sh rename to kvmd_data/etc/kvmd/clean_when_exit.sh diff --git a/configs/kvmd/custom_atx/gpio.sh b/kvmd_data/etc/kvmd/custom_atx/gpio.sh similarity index 100% rename from configs/kvmd/custom_atx/gpio.sh rename to kvmd_data/etc/kvmd/custom_atx/gpio.sh diff --git a/configs/kvmd/custom_atx/usbrelay_hid.py b/kvmd_data/etc/kvmd/custom_atx/usbrelay_hid.py similarity index 100% rename from configs/kvmd/custom_atx/usbrelay_hid.py rename to kvmd_data/etc/kvmd/custom_atx/usbrelay_hid.py diff --git a/configs/kvmd/custom_atx/usbrelay_hid.sh b/kvmd_data/etc/kvmd/custom_atx/usbrelay_hid.sh similarity index 100% rename from configs/kvmd/custom_atx/usbrelay_hid.sh rename to kvmd_data/etc/kvmd/custom_atx/usbrelay_hid.sh diff --git a/configs/kvmd/htpasswd b/kvmd_data/etc/kvmd/htpasswd similarity index 100% rename from configs/kvmd/htpasswd rename to kvmd_data/etc/kvmd/htpasswd diff --git a/configs/hw_info/model b/kvmd_data/etc/kvmd/hw_info/model similarity index 100% rename from configs/hw_info/model rename to kvmd_data/etc/kvmd/hw_info/model diff --git a/configs/hw_info/serial-number b/kvmd_data/etc/kvmd/hw_info/serial-number similarity index 100% rename from configs/hw_info/serial-number rename to kvmd_data/etc/kvmd/hw_info/serial-number diff --git a/configs/kvmd/ipmipasswd b/kvmd_data/etc/kvmd/ipmipasswd similarity index 100% rename from configs/kvmd/ipmipasswd rename to kvmd_data/etc/kvmd/ipmipasswd diff --git a/configs/janus/janus.jcfg b/kvmd_data/etc/kvmd/janus/janus.jcfg similarity index 100% rename from configs/janus/janus.jcfg rename to kvmd_data/etc/kvmd/janus/janus.jcfg diff --git a/configs/janus/janus.plugin.ustreamer.jcfg b/kvmd_data/etc/kvmd/janus/janus.plugin.ustreamer.jcfg similarity index 100% rename from configs/janus/janus.plugin.ustreamer.jcfg rename to kvmd_data/etc/kvmd/janus/janus.plugin.ustreamer.jcfg diff --git a/configs/janus/janus.transport.websockets.jcfg b/kvmd_data/etc/kvmd/janus/janus.transport.websockets.jcfg similarity index 100% rename from configs/janus/janus.transport.websockets.jcfg rename to kvmd_data/etc/kvmd/janus/janus.transport.websockets.jcfg diff --git a/configs/kvmd/kvm_input.sh b/kvmd_data/etc/kvmd/kvm_input.sh similarity index 97% rename from configs/kvmd/kvm_input.sh rename to kvmd_data/etc/kvmd/kvm_input.sh index e907d7ca..d66cf3eb 100755 --- a/configs/kvmd/kvm_input.sh +++ b/kvmd_data/etc/kvmd/kvm_input.sh @@ -19,7 +19,7 @@ else fi if [[ "$2" == "" ]]; then - IP="localhost" + IP="localhost:4430" else IP="$2" fi diff --git a/configs/kvmd/logging.yaml b/kvmd_data/etc/kvmd/logging.yaml similarity index 100% rename from configs/kvmd/logging.yaml rename to kvmd_data/etc/kvmd/logging.yaml diff --git a/configs/kvmd/main.yaml b/kvmd_data/etc/kvmd/main.yaml similarity index 100% rename from configs/kvmd/main.yaml rename to kvmd_data/etc/kvmd/main.yaml diff --git a/configs/kvmd/main/v4plus-hdmi-rpi4.yaml b/kvmd_data/etc/kvmd/main/v4plus-hdmi-rpi4.yaml similarity index 100% rename from configs/kvmd/main/v4plus-hdmi-rpi4.yaml rename to kvmd_data/etc/kvmd/main/v4plus-hdmi-rpi4.yaml diff --git a/configs/kvmd/meta.yaml b/kvmd_data/etc/kvmd/meta.yaml similarity index 91% rename from configs/kvmd/meta.yaml rename to kvmd_data/etc/kvmd/meta.yaml index 4127ffd0..f024a83f 100644 --- a/configs/kvmd/meta.yaml +++ b/kvmd_data/etc/kvmd/meta.yaml @@ -4,7 +4,7 @@ # will be displayed in the web interface. server: - host: localhost.localdomain + host: docker kvm: { base_on: PiKVM, diff --git a/configs/nginx/certbot.ctx-server.conf b/kvmd_data/etc/kvmd/nginx/certbot.ctx-server.conf similarity index 68% rename from configs/nginx/certbot.ctx-server.conf rename to kvmd_data/etc/kvmd/nginx/certbot.ctx-server.conf index 6c8ea525..fbc144b8 100644 --- a/configs/nginx/certbot.ctx-server.conf +++ b/kvmd_data/etc/kvmd/nginx/certbot.ctx-server.conf @@ -1,5 +1,5 @@ location /.well-known/acme-challenge { root /run/kvmd-certbot/webroot; - include /etc/kvmd/nginx/loc-nocache.conf; + include loc-nocache.conf; auth_request off; } diff --git a/kvmd_data/etc/kvmd/nginx/kvmd.ctx-http.conf b/kvmd_data/etc/kvmd/nginx/kvmd.ctx-http.conf new file mode 100644 index 00000000..899a2eaf --- /dev/null +++ b/kvmd_data/etc/kvmd/nginx/kvmd.ctx-http.conf @@ -0,0 +1,7 @@ +upstream kvmd { + server unix:/home/mofeng/kvmd_data/run/kvmd/kvmd.sock fail_timeout=0s max_fails=0; +} + +upstream ustreamer { + server unix:/home/mofeng/kvmd_data/run/kvmd/ustreamer.sock fail_timeout=0s max_fails=0; +} diff --git a/configs/nginx/kvmd.ctx-server.conf b/kvmd_data/etc/kvmd/nginx/kvmd.ctx-server.conf similarity index 63% rename from configs/nginx/kvmd.ctx-server.conf rename to kvmd_data/etc/kvmd/nginx/kvmd.ctx-server.conf index 335849bb..05c6a11c 100644 --- a/configs/nginx/kvmd.ctx-server.conf +++ b/kvmd_data/etc/kvmd/nginx/kvmd.ctx-server.conf @@ -13,9 +13,9 @@ location = /auth_check { } location / { - root /usr/share/kvmd/web; - include /etc/kvmd/nginx/loc-login.conf; - include /etc/kvmd/nginx/loc-nocache.conf; + root /home/mofeng/kvmd_data/usr/share/kvmd/web; + include loc-login.conf; + include loc-nocache.conf; } location @login { @@ -23,30 +23,30 @@ location @login { } location /login { - root /usr/share/kvmd/web; + root /home/mofeng/kvmd_data/usr/share/kvmd/web; auth_request off; } location /share { - root /usr/share/kvmd/web; - include /etc/kvmd/nginx/loc-nocache.conf; + root /home/mofeng/kvmd_data/usr/share/kvmd/web; + include loc-nocache.conf; auth_request off; } location = /share/css/user.css { - alias /etc/kvmd/web.css; + alias /home/mofeng/kvmd_data/etc/kvmd/web.css; auth_request off; } location = /favicon.ico { - alias /usr/share/kvmd/web/favicon.ico; - include /etc/kvmd/nginx/loc-nocache.conf; + alias /home/mofeng/kvmd_data/usr/share/kvmd/web/favicon.ico; + include loc-nocache.conf; auth_request off; } location = /robots.txt { - alias /usr/share/kvmd/web/robots.txt; - include /etc/kvmd/nginx/loc-nocache.conf; + alias /home/mofeng/kvmd_data/usr/share/kvmd/web/robots.txt; + include loc-nocache.conf; auth_request off; } @@ -54,8 +54,8 @@ location /api/ws { rewrite ^/api/ws$ /ws break; rewrite ^/api/ws\?(.*)$ /ws?$1 break; proxy_pass http://kvmd; - include /etc/kvmd/nginx/loc-proxy.conf; - include /etc/kvmd/nginx/loc-websocket.conf; + include loc-proxy.conf; + include loc-websocket.conf; auth_request off; } @@ -63,8 +63,8 @@ location /api/hid/print { rewrite ^/api/hid/print$ /hid/print break; rewrite ^/api/hid/print\?(.*)$ /hid/print?$1 break; proxy_pass http://kvmd; - include /etc/kvmd/nginx/loc-proxy.conf; - include /etc/kvmd/nginx/loc-bigpost.conf; + include loc-proxy.conf; + include loc-bigpost.conf; auth_request off; } @@ -72,8 +72,8 @@ location /api/msd/read { rewrite ^/api/msd/read$ /msd/read break; rewrite ^/api/msd/read\?(.*)$ /msd/read?$1 break; proxy_pass http://kvmd; - include /etc/kvmd/nginx/loc-proxy.conf; - include /etc/kvmd/nginx/loc-nobuffering.conf; + include loc-proxy.conf; + include loc-nobuffering.conf; proxy_read_timeout 7d; auth_request off; } @@ -82,8 +82,8 @@ location /api/msd/write_remote { rewrite ^/api/msd/write_remote$ /msd/write_remote break; rewrite ^/api/msd/write_remote\?(.*)$ /msd/write_remote?$1 break; proxy_pass http://kvmd; - include /etc/kvmd/nginx/loc-proxy.conf; - include /etc/kvmd/nginx/loc-nobuffering.conf; + include loc-proxy.conf; + include loc-nobuffering.conf; proxy_read_timeout 7d; auth_request off; } @@ -92,8 +92,8 @@ location /api/msd/write { rewrite ^/api/msd/write$ /msd/write break; rewrite ^/api/msd/write\?(.*)$ /msd/write?$1 break; proxy_pass http://kvmd; - include /etc/kvmd/nginx/loc-proxy.conf; - include /etc/kvmd/nginx/loc-bigpost.conf; + include loc-proxy.conf; + include loc-bigpost.conf; auth_request off; } @@ -101,8 +101,8 @@ location /api/log { rewrite ^/api/log$ /log break; rewrite ^/api/log\?(.*)$ /log?$1 break; proxy_pass http://kvmd; - include /etc/kvmd/nginx/loc-proxy.conf; - include /etc/kvmd/nginx/loc-nobuffering.conf; + include loc-proxy.conf; + include loc-nobuffering.conf; proxy_read_timeout 7d; auth_request off; } @@ -111,7 +111,7 @@ location /api { rewrite ^/api$ / break; rewrite ^/api/(.*)$ /$1 break; proxy_pass http://kvmd; - include /etc/kvmd/nginx/loc-proxy.conf; + include loc-proxy.conf; auth_request off; } @@ -120,12 +120,12 @@ location /streamer { rewrite ^/streamer\?(.*)$ ?$1 break; rewrite ^/streamer/(.*)$ /$1 break; proxy_pass http://ustreamer; - include /etc/kvmd/nginx/loc-proxy.conf; - include /etc/kvmd/nginx/loc-nobuffering.conf; + include loc-proxy.conf; + include loc-nobuffering.conf; } location /redfish { proxy_pass http://kvmd; - include /etc/kvmd/nginx/loc-proxy.conf; + include loc-proxy.conf; auth_request off; } diff --git a/configs/nginx/loc-bigpost.conf b/kvmd_data/etc/kvmd/nginx/loc-bigpost.conf similarity index 100% rename from configs/nginx/loc-bigpost.conf rename to kvmd_data/etc/kvmd/nginx/loc-bigpost.conf diff --git a/configs/nginx/loc-login.conf b/kvmd_data/etc/kvmd/nginx/loc-login.conf similarity index 100% rename from configs/nginx/loc-login.conf rename to kvmd_data/etc/kvmd/nginx/loc-login.conf diff --git a/configs/nginx/loc-nobuffering.conf b/kvmd_data/etc/kvmd/nginx/loc-nobuffering.conf similarity index 100% rename from configs/nginx/loc-nobuffering.conf rename to kvmd_data/etc/kvmd/nginx/loc-nobuffering.conf diff --git a/configs/nginx/loc-nocache.conf b/kvmd_data/etc/kvmd/nginx/loc-nocache.conf similarity index 100% rename from configs/nginx/loc-nocache.conf rename to kvmd_data/etc/kvmd/nginx/loc-nocache.conf diff --git a/configs/nginx/loc-proxy.conf b/kvmd_data/etc/kvmd/nginx/loc-proxy.conf similarity index 100% rename from configs/nginx/loc-proxy.conf rename to kvmd_data/etc/kvmd/nginx/loc-proxy.conf diff --git a/configs/nginx/loc-websocket.conf b/kvmd_data/etc/kvmd/nginx/loc-websocket.conf similarity index 100% rename from configs/nginx/loc-websocket.conf rename to kvmd_data/etc/kvmd/nginx/loc-websocket.conf diff --git a/configs/nginx/mime-types.conf b/kvmd_data/etc/kvmd/nginx/mime-types.conf similarity index 100% rename from configs/nginx/mime-types.conf rename to kvmd_data/etc/kvmd/nginx/mime-types.conf diff --git a/kvmd_data/etc/kvmd/nginx/nginx.conf b/kvmd_data/etc/kvmd/nginx/nginx.conf new file mode 100644 index 00000000..191d57f3 --- /dev/null +++ b/kvmd_data/etc/kvmd/nginx/nginx.conf @@ -0,0 +1,55 @@ +worker_processes 4; +pid /tmp/kvmd-nginx.pid; +# error_log /tmp/kvmd-nginx.error.log; +error_log stderr; + +include /usr/share/kvmd/extras/*/nginx.ctx-main.conf; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + types_hash_max_size 4096; + server_names_hash_bucket_size 128; + + access_log off; + + include mime-types.conf; + default_type application/octet-stream; + charset utf-8; + + sendfile on; + tcp_nodelay on; + tcp_nopush on; + keepalive_timeout 10; + client_max_body_size 4k; + + client_body_temp_path /tmp/kvmd-nginx/client_body_temp; + fastcgi_temp_path /tmp/kvmd-nginx/fastcgi_temp; + proxy_temp_path /tmp/kvmd-nginx/proxy_temp; + scgi_temp_path /tmp/kvmd-nginx/scgi_temp; + uwsgi_temp_path /tmp/kvmd-nginx/uwsgi_temp; + + include kvmd.ctx-http.conf; + include /usr/share/kvmd/extras/*/nginx.ctx-http.conf; + + + server { + listen 8080; + include certbot.ctx-server.conf; + location / { + return 301 https://$host:4430$request_uri; + } + } + + server { + listen 4430 ssl http2; + include ssl.conf; + include kvmd.ctx-server.conf; + include /usr/share/kvmd/extras/*/nginx.ctx-server.conf; + } + +} diff --git a/configs/nginx/nginx.conf.mako b/kvmd_data/etc/kvmd/nginx/nginx.conf.mako similarity index 100% rename from configs/nginx/nginx.conf.mako rename to kvmd_data/etc/kvmd/nginx/nginx.conf.mako diff --git a/configs/nginx/ssl.conf b/kvmd_data/etc/kvmd/nginx/ssl.conf similarity index 65% rename from configs/nginx/ssl.conf rename to kvmd_data/etc/kvmd/nginx/ssl.conf index 8d700f35..ffd0a018 100644 --- a/configs/nginx/ssl.conf +++ b/kvmd_data/etc/kvmd/nginx/ssl.conf @@ -1,5 +1,5 @@ ssl_protocols TLSv1.3 TLSv1.2 TLSv1.1 TLSv1; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; -ssl_certificate /etc/kvmd/nginx/ssl/server.crt; -ssl_certificate_key /etc/kvmd/nginx/ssl/server.key; +ssl_certificate ssl/server.crt; +ssl_certificate_key ssl/server.key; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; diff --git a/kvmd_data/etc/kvmd/nginx/ssl/server.crt b/kvmd_data/etc/kvmd/nginx/ssl/server.crt new file mode 100644 index 00000000..29c9fd7e --- /dev/null +++ b/kvmd_data/etc/kvmd/nginx/ssl/server.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIB1zCCAX2gAwIBAgIUfHNyb1wMmgGuQnbTZUYENGtW69UwCgYIKoZIzj0EAwIw +QTELMAkGA1UEBhMCVVMxDjAMBgNVBAoMBVBpS1ZNMQ4wDAYDVQQLDAVQaUtWTTES +MBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MTIzMDE2NTkyNFoXDTM0MTIyODE2NTky +NFowQTELMAkGA1UEBhMCVVMxDjAMBgNVBAoMBVBpS1ZNMQ4wDAYDVQQLDAVQaUtW +TTESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +TrSca0xuIbYA9axtmupRnu0nG5QVgkGBwOHr44SrJe/kTkRL/p0EC0iQo2ZXwork +6Z4kBNcUlYb2QkvBboqaWqNTMFEwHQYDVR0OBBYEFBhkv8k5r6L3eqz8zO6bQ0AT +mtFAMB8GA1UdIwQYMBaAFBhkv8k5r6L3eqz8zO6bQ0ATmtFAMA8GA1UdEwEB/wQF +MAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgbYxKLTqhxr13m75Vlil8a1uILYiv9j4D +cdTNXdoVTH0CIQCFSCXvt93o5GgNqhg0l3il66yuCqevHoDqOwQbP0/7PQ== +-----END CERTIFICATE----- diff --git a/kvmd_data/etc/kvmd/nginx/ssl/server.key b/kvmd_data/etc/kvmd/nginx/ssl/server.key new file mode 100644 index 00000000..e902a894 --- /dev/null +++ b/kvmd_data/etc/kvmd/nginx/ssl/server.key @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIF+vBzNTRb0YW7hzHSWPaLydFqN4TJCjPo5W+5T2u2k5oAoGCCqGSM49 +AwEHoUQDQgAETrSca0xuIbYA9axtmupRnu0nG5QVgkGBwOHr44SrJe/kTkRL/p0E +C0iQo2ZXwork6Z4kBNcUlYb2QkvBboqaWg== +-----END EC PRIVATE KEY----- diff --git a/configs/kvmd/override.yaml b/kvmd_data/etc/kvmd/override.yaml similarity index 72% rename from configs/kvmd/override.yaml rename to kvmd_data/etc/kvmd/override.yaml index a21e9064..0b9cd197 100644 --- a/configs/kvmd/override.yaml +++ b/kvmd_data/etc/kvmd/override.yaml @@ -1,6 +1,24 @@ kvmd: + info: + meta: kvmd_data/etc/kvmd/meta.yaml + extras: kvmd_data/usr/share/kvmd/extras + hw: + platform: kvmd_data/usr/share/kvmd/platform + vcgencmd_cmd: kvmd_data/usr/bin/vcgencmd auth: enabled: true + internal: + file: kvmd_data/etc/kvmd/htpasswd + + totp: + secret: + file: kvmd_data/etc/kvmd/totp.secret + + + server: + unix: kvmd_data/run/kvmd/kvmd.sock + #unix_mode: 432 + #unix_rm: true atx: type: disabled @@ -8,7 +26,7 @@ kvmd: hid: type: ch9329 device: /dev/ttyUSB0 - speed: 9600 + speed: 115200 read_timeout: 0.3 jiggler: @@ -18,6 +36,8 @@ kvmd: mouse_alt: device: /dev/kvmd-hid-mouse-alt + keymap: kvmd_data/usr/share/kvmd/keymaps/en-us + msd: #type: otg remount_cmd: /bin/true @@ -44,29 +64,27 @@ kvmd: default: 8000 cmd: - - "/usr/bin/ustreamer" + - "kvmd_data/usr/bin/ustreamer" - "--device=/dev/video0" - "--persistent" - "--format=mjpeg" - - "--encoder=LIBX264-VIDEO" - "--resolution={resolution}" - "--desired-fps={desired_fps}" - "--drop-same-frames=30" - "--last-as-blank=0" - "--unix={unix}" - "--unix-rm" - - "--unix-mode=0666" + - "--unix-mode=777" - "--exit-on-parent-death" - "--process-name-prefix={process_name_prefix}" - "--notify-parent" - "--no-log-colors" - - "--h264-sink=kvmd::ustreamer::h264" - - "--h264-sink-mode=0660" - "--jpeg-sink=kvmd::ustreamer::jpeg" - "--jpeg-sink-mode=0660" - - "--h264-bitrate={h264_bitrate}" - - "--h264-gop={h264_gop}" - "--slowdown" + + unix: kvmd_data/run/kvmd/ustreamer.sock + gpio: drivers: wol_server1: @@ -74,19 +92,19 @@ kvmd: mac: 2c:56:dc:db:7c:1e short_press: type: cmd - cmd: [/etc/kvmd/atx.sh, short] + cmd: [kvmd_data/etc/kvmd/atx.sh, short] long_press: type: cmd - cmd: [/etc/kvmd/atx.sh, long] + cmd: [kvmd_data/etc/kvmd/atx.sh, long] reset_press: type: cmd - cmd: [/etc/kvmd/atx.sh, reset] + cmd: [kvmd_data/etc/kvmd/atx.sh, reset] input1: type: cmd - cmd: [/etc/kvmd/kvm_input.sh, 1] + cmd: [kvmd_data/etc/kvmd/kvm_input.sh, 1] input2: type: cmd - cmd: [/etc/kvmd/kvm_input.sh, 2] + cmd: [kvmd_data/etc/kvmd/kvm_input.sh, 2] scheme: wol_server1: driver: wol_server1 @@ -133,21 +151,27 @@ kvmd: - ["#KVM 切换"] - ["#HDMI 1", input1-button|切换] - ["#HDMI 2", input2-button|切换] - +ipmi: + auth: + file: kvmd_data/etc/kvmd/ipmipasswd vnc: - keymap: /usr/share/kvmd/keymaps/en-us + keymap: kvmd_data/usr/share/kvmd/keymaps/en-us mouse_output: usb + server: + tls: + x509: + cert: kvmd_data/etc/kvmd/vnc/ssl/server.crt + key: kvmd_data/etc/kvmd/vnc/ssl/server.key auth: vncauth: enabled: true + file: kvmd_data/etc/kvmd/vncpasswd memsink: jpeg: sink: "kvmd::ustreamer::jpeg" - h264: - sink: "kvmd::ustreamer::h264" otgnet: commands: @@ -160,4 +184,8 @@ nginx: http: port: 8080 https: - port: 4430 \ No newline at end of file + port: 4430 + +janus: + cmd: + - "/bin/true" \ No newline at end of file diff --git a/configs/kvmd/supervisord.conf b/kvmd_data/etc/kvmd/supervisord.conf similarity index 61% rename from configs/kvmd/supervisord.conf rename to kvmd_data/etc/kvmd/supervisord.conf index 9867da32..f2e6fd63 100644 --- a/configs/kvmd/supervisord.conf +++ b/kvmd_data/etc/kvmd/supervisord.conf @@ -54,3 +54,36 @@ stdout_logfile=/dev/stdout stdout_logfile_maxbytes = 0 redirect_stderr=true + +[program:kvmd-webterm] +command=/usr/local/bin/ttyd --interface=/run/kvmd/ttyd.sock --port=0 --writable /bin/bash -c '/etc/kvmd/armbain-motd; bash' +directory=/ +autostart=true +autorestart=true +priority=14 +stopasgroup=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes = 0 +redirect_stderr=true + +[program:kvmd-vnc] +command=python -m kvmd.apps.vnc --run +directory=/ +autostart=true +autorestart=true +priority=11 +stopasgroup=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes = 0 +redirect_stderr=true + +[program:kvmd-ipmi] +command=python -m kvmd.apps.ipmi --run +directory=/ +autostart=true +autorestart=true +priority=12 +stopasgroup=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes = 0 +redirect_stderr=true diff --git a/configs/os/cmdline/v1-hdmiusb-rpi3.sed b/kvmd_data/etc/kvmd/totp.secret similarity index 100% rename from configs/os/cmdline/v1-hdmiusb-rpi3.sed rename to kvmd_data/etc/kvmd/totp.secret diff --git a/kvmd_data/etc/kvmd/vnc/ssl/server.crt b/kvmd_data/etc/kvmd/vnc/ssl/server.crt new file mode 100644 index 00000000..b967b5fe --- /dev/null +++ b/kvmd_data/etc/kvmd/vnc/ssl/server.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIB2DCCAX2gAwIBAgIUfRsPya5RSXxEDeSBi1iJiYfMfnQwCgYIKoZIzj0EAwIw +QTELMAkGA1UEBhMCVVMxDjAMBgNVBAoMBVBpS1ZNMQ4wDAYDVQQLDAVQaUtWTTES +MBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MTIzMDE2NTkyNFoXDTM0MTIyODE2NTky +NFowQTELMAkGA1UEBhMCVVMxDjAMBgNVBAoMBVBpS1ZNMQ4wDAYDVQQLDAVQaUtW +TTESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +8OfAJl4ck4EnmpO9LvvBusatRFPm5qCYM1Al5nxql647sy87PjBvbSM1L9CLuj1z +3wAQnGmFD1DDGyNAFm9uZ6NTMFEwHQYDVR0OBBYEFFZxf760XzrpI9nkte8AyuHZ +qs2+MB8GA1UdIwQYMBaAFFZxf760XzrpI9nkte8AyuHZqs2+MA8GA1UdEwEB/wQF +MAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIhAMc/ymoJgZ+d+7UnFOhr1UfUAayxirqM ++iaa0hGmBkqmAiEAscvZYM5Le2q0KdjPydcuTPBXi13q68zSDaib1jKQ0ZM= +-----END CERTIFICATE----- diff --git a/kvmd_data/etc/kvmd/vnc/ssl/server.key b/kvmd_data/etc/kvmd/vnc/ssl/server.key new file mode 100644 index 00000000..4812f973 --- /dev/null +++ b/kvmd_data/etc/kvmd/vnc/ssl/server.key @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIG08yMcwf2DxWHbTand3dE0hbQgk3uAv2FRl6Umyrz0boAoGCCqGSM49 +AwEHoUQDQgAE8OfAJl4ck4EnmpO9LvvBusatRFPm5qCYM1Al5nxql647sy87PjBv +bSM1L9CLuj1z3wAQnGmFD1DDGyNAFm9uZw== +-----END EC PRIVATE KEY----- diff --git a/configs/kvmd/vncpasswd b/kvmd_data/etc/kvmd/vncpasswd similarity index 100% rename from configs/kvmd/vncpasswd rename to kvmd_data/etc/kvmd/vncpasswd diff --git a/configs/kvmd/web.css b/kvmd_data/etc/kvmd/web.css similarity index 100% rename from configs/kvmd/web.css rename to kvmd_data/etc/kvmd/web.css diff --git a/kvmd_data/usr/bin/ustreamer b/kvmd_data/usr/bin/ustreamer new file mode 100755 index 0000000000000000000000000000000000000000..8c0a15719079818109cb6a4e9a4617286dedc9ff GIT binary patch literal 438592 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 Rar6_yX+Z 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 Sca0-B^N>n^sVjjiX#5}iblGYE literal 0 HcmV?d00001 diff --git a/kvmd_data/usr/bin/vcgencmd b/kvmd_data/usr/bin/vcgencmd new file mode 100755 index 00000000..3985bc1f --- /dev/null +++ b/kvmd_data/usr/bin/vcgencmd @@ -0,0 +1,4 @@ +#!/bin/sh +case $1 in + get_throttled) echo "throttled=0x0";; +esac diff --git a/extras/ipmi/manifest.yaml b/kvmd_data/usr/share/kvmd/extras/ipmi/manifest.yaml similarity index 100% rename from extras/ipmi/manifest.yaml rename to kvmd_data/usr/share/kvmd/extras/ipmi/manifest.yaml diff --git a/extras/janus-static/manifest.yaml b/kvmd_data/usr/share/kvmd/extras/janus-static/manifest.yaml similarity index 100% rename from extras/janus-static/manifest.yaml rename to kvmd_data/usr/share/kvmd/extras/janus-static/manifest.yaml diff --git a/extras/janus/manifest.yaml b/kvmd_data/usr/share/kvmd/extras/janus/manifest.yaml similarity index 100% rename from extras/janus/manifest.yaml rename to kvmd_data/usr/share/kvmd/extras/janus/manifest.yaml diff --git a/extras/janus/nginx.ctx-http.conf b/kvmd_data/usr/share/kvmd/extras/janus/nginx.ctx-http.conf similarity index 100% rename from extras/janus/nginx.ctx-http.conf rename to kvmd_data/usr/share/kvmd/extras/janus/nginx.ctx-http.conf diff --git a/extras/janus/nginx.ctx-server.conf b/kvmd_data/usr/share/kvmd/extras/janus/nginx.ctx-server.conf similarity index 100% rename from extras/janus/nginx.ctx-server.conf rename to kvmd_data/usr/share/kvmd/extras/janus/nginx.ctx-server.conf diff --git a/extras/vnc/manifest.yaml b/kvmd_data/usr/share/kvmd/extras/vnc/manifest.yaml similarity index 100% rename from extras/vnc/manifest.yaml rename to kvmd_data/usr/share/kvmd/extras/vnc/manifest.yaml diff --git a/extras/webterm/manifest.yaml b/kvmd_data/usr/share/kvmd/extras/webterm/manifest.yaml similarity index 100% rename from extras/webterm/manifest.yaml rename to kvmd_data/usr/share/kvmd/extras/webterm/manifest.yaml diff --git a/extras/webterm/nginx.ctx-http.conf b/kvmd_data/usr/share/kvmd/extras/webterm/nginx.ctx-http.conf similarity index 100% rename from extras/webterm/nginx.ctx-http.conf rename to kvmd_data/usr/share/kvmd/extras/webterm/nginx.ctx-http.conf diff --git a/extras/webterm/nginx.ctx-server.conf b/kvmd_data/usr/share/kvmd/extras/webterm/nginx.ctx-server.conf similarity index 100% rename from extras/webterm/nginx.ctx-server.conf rename to kvmd_data/usr/share/kvmd/extras/webterm/nginx.ctx-server.conf diff --git a/contrib/keymaps/ar b/kvmd_data/usr/share/kvmd/keymaps/ar similarity index 100% rename from contrib/keymaps/ar rename to kvmd_data/usr/share/kvmd/keymaps/ar diff --git a/contrib/keymaps/bepo b/kvmd_data/usr/share/kvmd/keymaps/bepo similarity index 100% rename from contrib/keymaps/bepo rename to kvmd_data/usr/share/kvmd/keymaps/bepo diff --git a/contrib/keymaps/cz b/kvmd_data/usr/share/kvmd/keymaps/cz similarity index 100% rename from contrib/keymaps/cz rename to kvmd_data/usr/share/kvmd/keymaps/cz diff --git a/contrib/keymaps/da b/kvmd_data/usr/share/kvmd/keymaps/da similarity index 100% rename from contrib/keymaps/da rename to kvmd_data/usr/share/kvmd/keymaps/da diff --git a/contrib/keymaps/de b/kvmd_data/usr/share/kvmd/keymaps/de similarity index 100% rename from contrib/keymaps/de rename to kvmd_data/usr/share/kvmd/keymaps/de diff --git a/contrib/keymaps/de-ch b/kvmd_data/usr/share/kvmd/keymaps/de-ch similarity index 100% rename from contrib/keymaps/de-ch rename to kvmd_data/usr/share/kvmd/keymaps/de-ch diff --git a/contrib/keymaps/en-gb b/kvmd_data/usr/share/kvmd/keymaps/en-gb similarity index 100% rename from contrib/keymaps/en-gb rename to kvmd_data/usr/share/kvmd/keymaps/en-gb diff --git a/contrib/keymaps/en-us b/kvmd_data/usr/share/kvmd/keymaps/en-us similarity index 100% rename from contrib/keymaps/en-us rename to kvmd_data/usr/share/kvmd/keymaps/en-us diff --git a/contrib/keymaps/en-us-altgr-intl b/kvmd_data/usr/share/kvmd/keymaps/en-us-altgr-intl similarity index 100% rename from contrib/keymaps/en-us-altgr-intl rename to kvmd_data/usr/share/kvmd/keymaps/en-us-altgr-intl diff --git a/contrib/keymaps/es b/kvmd_data/usr/share/kvmd/keymaps/es similarity index 100% rename from contrib/keymaps/es rename to kvmd_data/usr/share/kvmd/keymaps/es diff --git a/contrib/keymaps/et b/kvmd_data/usr/share/kvmd/keymaps/et similarity index 100% rename from contrib/keymaps/et rename to kvmd_data/usr/share/kvmd/keymaps/et diff --git a/contrib/keymaps/fi b/kvmd_data/usr/share/kvmd/keymaps/fi similarity index 100% rename from contrib/keymaps/fi rename to kvmd_data/usr/share/kvmd/keymaps/fi diff --git a/contrib/keymaps/fo b/kvmd_data/usr/share/kvmd/keymaps/fo similarity index 100% rename from contrib/keymaps/fo rename to kvmd_data/usr/share/kvmd/keymaps/fo diff --git a/contrib/keymaps/fr b/kvmd_data/usr/share/kvmd/keymaps/fr similarity index 100% rename from contrib/keymaps/fr rename to kvmd_data/usr/share/kvmd/keymaps/fr diff --git a/contrib/keymaps/fr-be b/kvmd_data/usr/share/kvmd/keymaps/fr-be similarity index 100% rename from contrib/keymaps/fr-be rename to kvmd_data/usr/share/kvmd/keymaps/fr-be diff --git a/contrib/keymaps/fr-ca b/kvmd_data/usr/share/kvmd/keymaps/fr-ca similarity index 100% rename from contrib/keymaps/fr-ca rename to kvmd_data/usr/share/kvmd/keymaps/fr-ca diff --git a/contrib/keymaps/fr-ch b/kvmd_data/usr/share/kvmd/keymaps/fr-ch similarity index 100% rename from contrib/keymaps/fr-ch rename to kvmd_data/usr/share/kvmd/keymaps/fr-ch diff --git a/contrib/keymaps/hr b/kvmd_data/usr/share/kvmd/keymaps/hr similarity index 100% rename from contrib/keymaps/hr rename to kvmd_data/usr/share/kvmd/keymaps/hr diff --git a/contrib/keymaps/hu b/kvmd_data/usr/share/kvmd/keymaps/hu similarity index 100% rename from contrib/keymaps/hu rename to kvmd_data/usr/share/kvmd/keymaps/hu diff --git a/contrib/keymaps/is b/kvmd_data/usr/share/kvmd/keymaps/is similarity index 100% rename from contrib/keymaps/is rename to kvmd_data/usr/share/kvmd/keymaps/is diff --git a/contrib/keymaps/it b/kvmd_data/usr/share/kvmd/keymaps/it similarity index 100% rename from contrib/keymaps/it rename to kvmd_data/usr/share/kvmd/keymaps/it diff --git a/contrib/keymaps/ja b/kvmd_data/usr/share/kvmd/keymaps/ja similarity index 100% rename from contrib/keymaps/ja rename to kvmd_data/usr/share/kvmd/keymaps/ja diff --git a/contrib/keymaps/lt b/kvmd_data/usr/share/kvmd/keymaps/lt similarity index 100% rename from contrib/keymaps/lt rename to kvmd_data/usr/share/kvmd/keymaps/lt diff --git a/contrib/keymaps/lv b/kvmd_data/usr/share/kvmd/keymaps/lv similarity index 100% rename from contrib/keymaps/lv rename to kvmd_data/usr/share/kvmd/keymaps/lv diff --git a/contrib/keymaps/mk b/kvmd_data/usr/share/kvmd/keymaps/mk similarity index 100% rename from contrib/keymaps/mk rename to kvmd_data/usr/share/kvmd/keymaps/mk diff --git a/contrib/keymaps/nl b/kvmd_data/usr/share/kvmd/keymaps/nl similarity index 100% rename from contrib/keymaps/nl rename to kvmd_data/usr/share/kvmd/keymaps/nl diff --git a/contrib/keymaps/no b/kvmd_data/usr/share/kvmd/keymaps/no similarity index 100% rename from contrib/keymaps/no rename to kvmd_data/usr/share/kvmd/keymaps/no diff --git a/contrib/keymaps/pl b/kvmd_data/usr/share/kvmd/keymaps/pl similarity index 100% rename from contrib/keymaps/pl rename to kvmd_data/usr/share/kvmd/keymaps/pl diff --git a/contrib/keymaps/pt b/kvmd_data/usr/share/kvmd/keymaps/pt similarity index 100% rename from contrib/keymaps/pt rename to kvmd_data/usr/share/kvmd/keymaps/pt diff --git a/contrib/keymaps/pt-br b/kvmd_data/usr/share/kvmd/keymaps/pt-br similarity index 100% rename from contrib/keymaps/pt-br rename to kvmd_data/usr/share/kvmd/keymaps/pt-br diff --git a/contrib/keymaps/ru b/kvmd_data/usr/share/kvmd/keymaps/ru similarity index 100% rename from contrib/keymaps/ru rename to kvmd_data/usr/share/kvmd/keymaps/ru diff --git a/contrib/keymaps/sl b/kvmd_data/usr/share/kvmd/keymaps/sl similarity index 100% rename from contrib/keymaps/sl rename to kvmd_data/usr/share/kvmd/keymaps/sl diff --git a/contrib/keymaps/sv b/kvmd_data/usr/share/kvmd/keymaps/sv similarity index 100% rename from contrib/keymaps/sv rename to kvmd_data/usr/share/kvmd/keymaps/sv diff --git a/contrib/keymaps/th b/kvmd_data/usr/share/kvmd/keymaps/th similarity index 100% rename from contrib/keymaps/th rename to kvmd_data/usr/share/kvmd/keymaps/th diff --git a/contrib/keymaps/tr b/kvmd_data/usr/share/kvmd/keymaps/tr similarity index 100% rename from contrib/keymaps/tr rename to kvmd_data/usr/share/kvmd/keymaps/tr diff --git a/scripts/kvmd-gencert b/kvmd_data/usr/share/kvmd/kvmd-gencert similarity index 100% rename from scripts/kvmd-gencert rename to kvmd_data/usr/share/kvmd/kvmd-gencert diff --git a/kvmd_data/usr/share/kvmd/platform b/kvmd_data/usr/share/kvmd/platform new file mode 100644 index 00000000..0f00370c --- /dev/null +++ b/kvmd_data/usr/share/kvmd/platform @@ -0,0 +1,3 @@ +PIKVM_MODEL=docker_model +PIKVM_VIDEO=docker_video +PIKVM_BOARD=docker_board diff --git a/web/base.pug b/kvmd_data/usr/share/kvmd/web/base.pug similarity index 100% rename from web/base.pug rename to kvmd_data/usr/share/kvmd/web/base.pug diff --git a/hid/arduino/lib/.gitignore b/kvmd_data/usr/share/kvmd/web/extras/.gitignore similarity index 100% rename from hid/arduino/lib/.gitignore rename to kvmd_data/usr/share/kvmd/web/extras/.gitignore diff --git a/web/extras/webterm/terminal.svg b/kvmd_data/usr/share/kvmd/web/extras/webterm/terminal.svg similarity index 100% rename from web/extras/webterm/terminal.svg rename to kvmd_data/usr/share/kvmd/web/extras/webterm/terminal.svg diff --git a/web/favicon.ico b/kvmd_data/usr/share/kvmd/web/favicon.ico similarity index 100% rename from web/favicon.ico rename to kvmd_data/usr/share/kvmd/web/favicon.ico diff --git a/web/index.html b/kvmd_data/usr/share/kvmd/web/index.html similarity index 100% rename from web/index.html rename to kvmd_data/usr/share/kvmd/web/index.html diff --git a/web/index.pug b/kvmd_data/usr/share/kvmd/web/index.pug similarity index 100% rename from web/index.pug rename to kvmd_data/usr/share/kvmd/web/index.pug diff --git a/web/ipmi/index.html b/kvmd_data/usr/share/kvmd/web/ipmi/index.html similarity index 100% rename from web/ipmi/index.html rename to kvmd_data/usr/share/kvmd/web/ipmi/index.html diff --git a/web/ipmi/index.pug b/kvmd_data/usr/share/kvmd/web/ipmi/index.pug similarity index 100% rename from web/ipmi/index.pug rename to kvmd_data/usr/share/kvmd/web/ipmi/index.pug diff --git a/web/kvm/index.html b/kvmd_data/usr/share/kvmd/web/kvm/index.html similarity index 100% rename from web/kvm/index.html rename to kvmd_data/usr/share/kvmd/web/kvm/index.html diff --git a/web/kvm/index.pug b/kvmd_data/usr/share/kvmd/web/kvm/index.pug similarity index 100% rename from web/kvm/index.pug rename to kvmd_data/usr/share/kvmd/web/kvm/index.pug diff --git a/web/kvm/navbar-atx.pug b/kvmd_data/usr/share/kvmd/web/kvm/navbar-atx.pug similarity index 100% rename from web/kvm/navbar-atx.pug rename to kvmd_data/usr/share/kvmd/web/kvm/navbar-atx.pug diff --git a/web/kvm/navbar-gpio.pug b/kvmd_data/usr/share/kvmd/web/kvm/navbar-gpio.pug similarity index 100% rename from web/kvm/navbar-gpio.pug rename to kvmd_data/usr/share/kvmd/web/kvm/navbar-gpio.pug diff --git a/web/kvm/navbar-health.pug b/kvmd_data/usr/share/kvmd/web/kvm/navbar-health.pug similarity index 100% rename from web/kvm/navbar-health.pug rename to kvmd_data/usr/share/kvmd/web/kvm/navbar-health.pug diff --git a/web/kvm/navbar-macro.pug b/kvmd_data/usr/share/kvmd/web/kvm/navbar-macro.pug similarity index 100% rename from web/kvm/navbar-macro.pug rename to kvmd_data/usr/share/kvmd/web/kvm/navbar-macro.pug diff --git a/web/kvm/navbar-msd.pug b/kvmd_data/usr/share/kvmd/web/kvm/navbar-msd.pug similarity index 100% rename from web/kvm/navbar-msd.pug rename to kvmd_data/usr/share/kvmd/web/kvm/navbar-msd.pug diff --git a/web/kvm/navbar-shortcuts.pug b/kvmd_data/usr/share/kvmd/web/kvm/navbar-shortcuts.pug similarity index 100% rename from web/kvm/navbar-shortcuts.pug rename to kvmd_data/usr/share/kvmd/web/kvm/navbar-shortcuts.pug diff --git a/web/kvm/navbar-system.pug b/kvmd_data/usr/share/kvmd/web/kvm/navbar-system.pug similarity index 100% rename from web/kvm/navbar-system.pug rename to kvmd_data/usr/share/kvmd/web/kvm/navbar-system.pug diff --git a/web/kvm/navbar-text.pug b/kvmd_data/usr/share/kvmd/web/kvm/navbar-text.pug similarity index 100% rename from web/kvm/navbar-text.pug rename to kvmd_data/usr/share/kvmd/web/kvm/navbar-text.pug diff --git a/web/kvm/navbar.pug b/kvmd_data/usr/share/kvmd/web/kvm/navbar.pug similarity index 100% rename from web/kvm/navbar.pug rename to kvmd_data/usr/share/kvmd/web/kvm/navbar.pug diff --git a/web/kvm/window-about.pug b/kvmd_data/usr/share/kvmd/web/kvm/window-about.pug similarity index 100% rename from web/kvm/window-about.pug rename to kvmd_data/usr/share/kvmd/web/kvm/window-about.pug diff --git a/web/kvm/window-keyboard.pug b/kvmd_data/usr/share/kvmd/web/kvm/window-keyboard.pug similarity index 100% rename from web/kvm/window-keyboard.pug rename to kvmd_data/usr/share/kvmd/web/kvm/window-keyboard.pug diff --git a/web/kvm/window-stream.pug b/kvmd_data/usr/share/kvmd/web/kvm/window-stream.pug similarity index 100% rename from web/kvm/window-stream.pug rename to kvmd_data/usr/share/kvmd/web/kvm/window-stream.pug diff --git a/web/kvm/window-webterm.pug b/kvmd_data/usr/share/kvmd/web/kvm/window-webterm.pug similarity index 100% rename from web/kvm/window-webterm.pug rename to kvmd_data/usr/share/kvmd/web/kvm/window-webterm.pug diff --git a/web/kvm/windows.pug b/kvmd_data/usr/share/kvmd/web/kvm/windows.pug similarity index 100% rename from web/kvm/windows.pug rename to kvmd_data/usr/share/kvmd/web/kvm/windows.pug diff --git a/web/login/index.html b/kvmd_data/usr/share/kvmd/web/login/index.html similarity index 100% rename from web/login/index.html rename to kvmd_data/usr/share/kvmd/web/login/index.html diff --git a/web/login/index.pug b/kvmd_data/usr/share/kvmd/web/login/index.pug similarity index 100% rename from web/login/index.pug rename to kvmd_data/usr/share/kvmd/web/login/index.pug diff --git a/web/robots.txt b/kvmd_data/usr/share/kvmd/web/robots.txt similarity index 100% rename from web/robots.txt rename to kvmd_data/usr/share/kvmd/web/robots.txt diff --git a/web/share/android-chrome-192x192.png b/kvmd_data/usr/share/kvmd/web/share/android-chrome-192x192.png similarity index 100% rename from web/share/android-chrome-192x192.png rename to kvmd_data/usr/share/kvmd/web/share/android-chrome-192x192.png diff --git a/web/share/apple-touch-icon.png b/kvmd_data/usr/share/kvmd/web/share/apple-touch-icon.png similarity index 100% rename from web/share/apple-touch-icon.png rename to kvmd_data/usr/share/kvmd/web/share/apple-touch-icon.png diff --git a/web/share/css/index/index.css b/kvmd_data/usr/share/kvmd/web/share/css/index/index.css similarity index 100% rename from web/share/css/index/index.css rename to kvmd_data/usr/share/kvmd/web/share/css/index/index.css diff --git a/web/share/css/keypad.css b/kvmd_data/usr/share/kvmd/web/share/css/keypad.css similarity index 100% rename from web/share/css/keypad.css rename to kvmd_data/usr/share/kvmd/web/share/css/keypad.css diff --git a/web/share/css/kvm/about.css b/kvmd_data/usr/share/kvmd/web/share/css/kvm/about.css similarity index 100% rename from web/share/css/kvm/about.css rename to kvmd_data/usr/share/kvmd/web/share/css/kvm/about.css diff --git a/web/share/css/kvm/hid.css b/kvmd_data/usr/share/kvmd/web/share/css/kvm/hid.css similarity index 100% rename from web/share/css/kvm/hid.css rename to kvmd_data/usr/share/kvmd/web/share/css/kvm/hid.css diff --git a/web/share/css/kvm/keyboard.css b/kvmd_data/usr/share/kvmd/web/share/css/kvm/keyboard.css similarity index 100% rename from web/share/css/kvm/keyboard.css rename to kvmd_data/usr/share/kvmd/web/share/css/kvm/keyboard.css diff --git a/web/share/css/kvm/msd.css b/kvmd_data/usr/share/kvmd/web/share/css/kvm/msd.css similarity index 100% rename from web/share/css/kvm/msd.css rename to kvmd_data/usr/share/kvmd/web/share/css/kvm/msd.css diff --git a/web/share/css/kvm/stream.css b/kvmd_data/usr/share/kvmd/web/share/css/kvm/stream.css similarity index 100% rename from web/share/css/kvm/stream.css rename to kvmd_data/usr/share/kvmd/web/share/css/kvm/stream.css diff --git a/web/share/css/kvm/system.css b/kvmd_data/usr/share/kvmd/web/share/css/kvm/system.css similarity index 100% rename from web/share/css/kvm/system.css rename to kvmd_data/usr/share/kvmd/web/share/css/kvm/system.css diff --git a/web/share/css/kvm/x-mobile.css b/kvmd_data/usr/share/kvmd/web/share/css/kvm/x-mobile.css similarity index 100% rename from web/share/css/kvm/x-mobile.css rename to kvmd_data/usr/share/kvmd/web/share/css/kvm/x-mobile.css diff --git a/web/share/css/led.css b/kvmd_data/usr/share/kvmd/web/share/css/led.css similarity index 100% rename from web/share/css/led.css rename to kvmd_data/usr/share/kvmd/web/share/css/led.css diff --git a/web/share/css/login/login.css b/kvmd_data/usr/share/kvmd/web/share/css/login/login.css similarity index 100% rename from web/share/css/login/login.css rename to kvmd_data/usr/share/kvmd/web/share/css/login/login.css diff --git a/web/share/css/main.css b/kvmd_data/usr/share/kvmd/web/share/css/main.css similarity index 100% rename from web/share/css/main.css rename to kvmd_data/usr/share/kvmd/web/share/css/main.css diff --git a/web/share/css/modal.css b/kvmd_data/usr/share/kvmd/web/share/css/modal.css similarity index 100% rename from web/share/css/modal.css rename to kvmd_data/usr/share/kvmd/web/share/css/modal.css diff --git a/web/share/css/navbar.css b/kvmd_data/usr/share/kvmd/web/share/css/navbar.css similarity index 100% rename from web/share/css/navbar.css rename to kvmd_data/usr/share/kvmd/web/share/css/navbar.css diff --git a/web/share/css/progress.css b/kvmd_data/usr/share/kvmd/web/share/css/progress.css similarity index 100% rename from web/share/css/progress.css rename to kvmd_data/usr/share/kvmd/web/share/css/progress.css diff --git a/web/share/css/radio.css b/kvmd_data/usr/share/kvmd/web/share/css/radio.css similarity index 100% rename from web/share/css/radio.css rename to kvmd_data/usr/share/kvmd/web/share/css/radio.css diff --git a/web/share/css/slider.css b/kvmd_data/usr/share/kvmd/web/share/css/slider.css similarity index 100% rename from web/share/css/slider.css rename to kvmd_data/usr/share/kvmd/web/share/css/slider.css diff --git a/web/share/css/start.css b/kvmd_data/usr/share/kvmd/web/share/css/start.css similarity index 100% rename from web/share/css/start.css rename to kvmd_data/usr/share/kvmd/web/share/css/start.css diff --git a/web/share/css/switch.css b/kvmd_data/usr/share/kvmd/web/share/css/switch.css similarity index 100% rename from web/share/css/switch.css rename to kvmd_data/usr/share/kvmd/web/share/css/switch.css diff --git a/web/share/css/tabs.css b/kvmd_data/usr/share/kvmd/web/share/css/tabs.css similarity index 100% rename from web/share/css/tabs.css rename to kvmd_data/usr/share/kvmd/web/share/css/tabs.css diff --git a/configs/os/cmdline/v1-hdmiusb-zero2w.sed b/kvmd_data/usr/share/kvmd/web/share/css/user.css similarity index 100% rename from configs/os/cmdline/v1-hdmiusb-zero2w.sed rename to kvmd_data/usr/share/kvmd/web/share/css/user.css diff --git a/web/share/css/vars.css b/kvmd_data/usr/share/kvmd/web/share/css/vars.css similarity index 100% rename from web/share/css/vars.css rename to kvmd_data/usr/share/kvmd/web/share/css/vars.css diff --git a/web/share/css/window.css b/kvmd_data/usr/share/kvmd/web/share/css/window.css similarity index 100% rename from web/share/css/window.css rename to kvmd_data/usr/share/kvmd/web/share/css/window.css diff --git a/web/share/css/x-desktop.css b/kvmd_data/usr/share/kvmd/web/share/css/x-desktop.css similarity index 100% rename from web/share/css/x-desktop.css rename to kvmd_data/usr/share/kvmd/web/share/css/x-desktop.css diff --git a/web/share/css/x-mobile.css b/kvmd_data/usr/share/kvmd/web/share/css/x-mobile.css similarity index 100% rename from web/share/css/x-mobile.css rename to kvmd_data/usr/share/kvmd/web/share/css/x-mobile.css diff --git a/web/share/favicon-16x16.png b/kvmd_data/usr/share/kvmd/web/share/favicon-16x16.png similarity index 100% rename from web/share/favicon-16x16.png rename to kvmd_data/usr/share/kvmd/web/share/favicon-16x16.png diff --git a/web/share/favicon-32x32.png b/kvmd_data/usr/share/kvmd/web/share/favicon-32x32.png similarity index 100% rename from web/share/favicon-32x32.png rename to kvmd_data/usr/share/kvmd/web/share/favicon-32x32.png diff --git a/web/share/i18n/i18n_en.json b/kvmd_data/usr/share/kvmd/web/share/i18n/i18n_en.json similarity index 100% rename from web/share/i18n/i18n_en.json rename to kvmd_data/usr/share/kvmd/web/share/i18n/i18n_en.json diff --git a/web/share/i18n/i18n_zh.json b/kvmd_data/usr/share/kvmd/web/share/i18n/i18n_zh.json similarity index 100% rename from web/share/i18n/i18n_zh.json rename to kvmd_data/usr/share/kvmd/web/share/i18n/i18n_zh.json diff --git a/web/share/js/bb.js b/kvmd_data/usr/share/kvmd/web/share/js/bb.js similarity index 100% rename from web/share/js/bb.js rename to kvmd_data/usr/share/kvmd/web/share/js/bb.js diff --git a/web/share/js/i18n/i18n.js b/kvmd_data/usr/share/kvmd/web/share/js/i18n/i18n.js similarity index 100% rename from web/share/js/i18n/i18n.js rename to kvmd_data/usr/share/kvmd/web/share/js/i18n/i18n.js diff --git a/web/share/js/i18n/jquery-3.7.1.min.js b/kvmd_data/usr/share/kvmd/web/share/js/i18n/jquery-3.7.1.min.js similarity index 100% rename from web/share/js/i18n/jquery-3.7.1.min.js rename to kvmd_data/usr/share/kvmd/web/share/js/i18n/jquery-3.7.1.min.js diff --git a/web/share/js/i18n/jquery.i18n.min.js b/kvmd_data/usr/share/kvmd/web/share/js/i18n/jquery.i18n.min.js similarity index 100% rename from web/share/js/i18n/jquery.i18n.min.js rename to kvmd_data/usr/share/kvmd/web/share/js/i18n/jquery.i18n.min.js diff --git a/web/share/js/index/main.js b/kvmd_data/usr/share/kvmd/web/share/js/index/main.js similarity index 100% rename from web/share/js/index/main.js rename to kvmd_data/usr/share/kvmd/web/share/js/index/main.js diff --git a/web/share/js/ipmi/main.js b/kvmd_data/usr/share/kvmd/web/share/js/ipmi/main.js similarity index 100% rename from web/share/js/ipmi/main.js rename to kvmd_data/usr/share/kvmd/web/share/js/ipmi/main.js diff --git a/web/share/js/keypad.js b/kvmd_data/usr/share/kvmd/web/share/js/keypad.js similarity index 100% rename from web/share/js/keypad.js rename to kvmd_data/usr/share/kvmd/web/share/js/keypad.js diff --git a/kvmd_data/usr/share/kvmd/web/share/js/kvm/adapter.js b/kvmd_data/usr/share/kvmd/web/share/js/kvm/adapter.js new file mode 100644 index 00000000..51d0f276 --- /dev/null +++ b/kvmd_data/usr/share/kvmd/web/share/js/kvm/adapter.js @@ -0,0 +1,3364 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0 && arguments[0] !== undefined ? arguments[0] : {}, + window = _ref.window; + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { + shimChrome: true, + shimFirefox: true, + shimSafari: true + }; + // Utils. + var logging = utils.log; + var browserDetails = utils.detectBrowser(window); + var adapter = { + browserDetails: browserDetails, + commonShim: commonShim, + extractVersion: utils.extractVersion, + disableLog: utils.disableLog, + disableWarnings: utils.disableWarnings, + // Expose sdp as a convenience. For production apps include directly. + sdp: sdp + }; + + // Shim browser if found. + switch (browserDetails.browser) { + case 'chrome': + if (!chromeShim || !chromeShim.shimPeerConnection || !options.shimChrome) { + logging('Chrome shim is not included in this adapter release.'); + return adapter; + } + if (browserDetails.version === null) { + logging('Chrome shim can not determine version, not shimming.'); + return adapter; + } + logging('adapter.js shimming chrome.'); + // Export to the adapter global object visible in the browser. + adapter.browserShim = chromeShim; + + // Must be called before shimPeerConnection. + commonShim.shimAddIceCandidateNullOrEmpty(window, browserDetails); + commonShim.shimParameterlessSetLocalDescription(window, browserDetails); + chromeShim.shimGetUserMedia(window, browserDetails); + chromeShim.shimMediaStream(window, browserDetails); + chromeShim.shimPeerConnection(window, browserDetails); + chromeShim.shimOnTrack(window, browserDetails); + chromeShim.shimAddTrackRemoveTrack(window, browserDetails); + chromeShim.shimGetSendersWithDtmf(window, browserDetails); + chromeShim.shimGetStats(window, browserDetails); + chromeShim.shimSenderReceiverGetStats(window, browserDetails); + chromeShim.fixNegotiationNeeded(window, browserDetails); + commonShim.shimRTCIceCandidate(window, browserDetails); + commonShim.shimRTCIceCandidateRelayProtocol(window, browserDetails); + commonShim.shimConnectionState(window, browserDetails); + commonShim.shimMaxMessageSize(window, browserDetails); + commonShim.shimSendThrowTypeError(window, browserDetails); + commonShim.removeExtmapAllowMixed(window, browserDetails); + break; + case 'firefox': + if (!firefoxShim || !firefoxShim.shimPeerConnection || !options.shimFirefox) { + logging('Firefox shim is not included in this adapter release.'); + return adapter; + } + logging('adapter.js shimming firefox.'); + // Export to the adapter global object visible in the browser. + adapter.browserShim = firefoxShim; + + // Must be called before shimPeerConnection. + commonShim.shimAddIceCandidateNullOrEmpty(window, browserDetails); + commonShim.shimParameterlessSetLocalDescription(window, browserDetails); + firefoxShim.shimGetUserMedia(window, browserDetails); + firefoxShim.shimPeerConnection(window, browserDetails); + firefoxShim.shimOnTrack(window, browserDetails); + firefoxShim.shimRemoveStream(window, browserDetails); + firefoxShim.shimSenderGetStats(window, browserDetails); + firefoxShim.shimReceiverGetStats(window, browserDetails); + firefoxShim.shimRTCDataChannel(window, browserDetails); + firefoxShim.shimAddTransceiver(window, browserDetails); + firefoxShim.shimGetParameters(window, browserDetails); + firefoxShim.shimCreateOffer(window, browserDetails); + firefoxShim.shimCreateAnswer(window, browserDetails); + commonShim.shimRTCIceCandidate(window, browserDetails); + commonShim.shimConnectionState(window, browserDetails); + commonShim.shimMaxMessageSize(window, browserDetails); + commonShim.shimSendThrowTypeError(window, browserDetails); + break; + case 'safari': + if (!safariShim || !options.shimSafari) { + logging('Safari shim is not included in this adapter release.'); + return adapter; + } + logging('adapter.js shimming safari.'); + // Export to the adapter global object visible in the browser. + adapter.browserShim = safariShim; + + // Must be called before shimCallbackAPI. + commonShim.shimAddIceCandidateNullOrEmpty(window, browserDetails); + commonShim.shimParameterlessSetLocalDescription(window, browserDetails); + safariShim.shimRTCIceServerUrls(window, browserDetails); + safariShim.shimCreateOfferLegacy(window, browserDetails); + safariShim.shimCallbacksAPI(window, browserDetails); + safariShim.shimLocalStreamsAPI(window, browserDetails); + safariShim.shimRemoteStreamsAPI(window, browserDetails); + safariShim.shimTrackEventTransceiver(window, browserDetails); + safariShim.shimGetUserMedia(window, browserDetails); + safariShim.shimAudioContext(window, browserDetails); + commonShim.shimRTCIceCandidate(window, browserDetails); + commonShim.shimRTCIceCandidateRelayProtocol(window, browserDetails); + commonShim.shimMaxMessageSize(window, browserDetails); + commonShim.shimSendThrowTypeError(window, browserDetails); + commonShim.removeExtmapAllowMixed(window, browserDetails); + break; + default: + logging('Unsupported browser!'); + break; + } + return adapter; +} + +},{"./chrome/chrome_shim":3,"./common_shim":6,"./firefox/firefox_shim":7,"./safari/safari_shim":10,"./utils":11,"sdp":12}],3:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ +/* eslint-env node */ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.fixNegotiationNeeded = fixNegotiationNeeded; +exports.shimAddTrackRemoveTrack = shimAddTrackRemoveTrack; +exports.shimAddTrackRemoveTrackWithNative = shimAddTrackRemoveTrackWithNative; +Object.defineProperty(exports, "shimGetDisplayMedia", { + enumerable: true, + get: function get() { + return _getdisplaymedia.shimGetDisplayMedia; + } +}); +exports.shimGetSendersWithDtmf = shimGetSendersWithDtmf; +exports.shimGetStats = shimGetStats; +Object.defineProperty(exports, "shimGetUserMedia", { + enumerable: true, + get: function get() { + return _getusermedia.shimGetUserMedia; + } +}); +exports.shimMediaStream = shimMediaStream; +exports.shimOnTrack = shimOnTrack; +exports.shimPeerConnection = shimPeerConnection; +exports.shimSenderReceiverGetStats = shimSenderReceiverGetStats; +var utils = _interopRequireWildcard(require("../utils.js")); +var _getusermedia = require("./getusermedia"); +var _getdisplaymedia = require("./getdisplaymedia"); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } +function shimMediaStream(window) { + window.MediaStream = window.MediaStream || window.webkitMediaStream; +} +function shimOnTrack(window) { + if (_typeof(window) === 'object' && window.RTCPeerConnection && !('ontrack' in window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { + get: function get() { + return this._ontrack; + }, + set: function set(f) { + if (this._ontrack) { + this.removeEventListener('track', this._ontrack); + } + this.addEventListener('track', this._ontrack = f); + }, + enumerable: true, + configurable: true + }); + var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; + window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() { + var _this = this; + if (!this._ontrackpoly) { + this._ontrackpoly = function (e) { + // onaddstream does not fire when a track is added to an existing + // stream. But stream.onaddtrack is implemented so we use that. + e.stream.addEventListener('addtrack', function (te) { + var receiver; + if (window.RTCPeerConnection.prototype.getReceivers) { + receiver = _this.getReceivers().find(function (r) { + return r.track && r.track.id === te.track.id; + }); + } else { + receiver = { + track: te.track + }; + } + var event = new Event('track'); + event.track = te.track; + event.receiver = receiver; + event.transceiver = { + receiver: receiver + }; + event.streams = [e.stream]; + _this.dispatchEvent(event); + }); + e.stream.getTracks().forEach(function (track) { + var receiver; + if (window.RTCPeerConnection.prototype.getReceivers) { + receiver = _this.getReceivers().find(function (r) { + return r.track && r.track.id === track.id; + }); + } else { + receiver = { + track: track + }; + } + var event = new Event('track'); + event.track = track; + event.receiver = receiver; + event.transceiver = { + receiver: receiver + }; + event.streams = [e.stream]; + _this.dispatchEvent(event); + }); + }; + this.addEventListener('addstream', this._ontrackpoly); + } + return origSetRemoteDescription.apply(this, arguments); + }; + } else { + // even if RTCRtpTransceiver is in window, it is only used and + // emitted in unified-plan. Unfortunately this means we need + // to unconditionally wrap the event. + utils.wrapPeerConnectionEvent(window, 'track', function (e) { + if (!e.transceiver) { + Object.defineProperty(e, 'transceiver', { + value: { + receiver: e.receiver + } + }); + } + return e; + }); + } +} +function shimGetSendersWithDtmf(window) { + // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack. + if (_typeof(window) === 'object' && window.RTCPeerConnection && !('getSenders' in window.RTCPeerConnection.prototype) && 'createDTMFSender' in window.RTCPeerConnection.prototype) { + var shimSenderWithDtmf = function shimSenderWithDtmf(pc, track) { + return { + track: track, + get dtmf() { + if (this._dtmf === undefined) { + if (track.kind === 'audio') { + this._dtmf = pc.createDTMFSender(track); + } else { + this._dtmf = null; + } + } + return this._dtmf; + }, + _pc: pc + }; + }; + + // augment addTrack when getSenders is not available. + if (!window.RTCPeerConnection.prototype.getSenders) { + window.RTCPeerConnection.prototype.getSenders = function getSenders() { + this._senders = this._senders || []; + return this._senders.slice(); // return a copy of the internal state. + }; + + var origAddTrack = window.RTCPeerConnection.prototype.addTrack; + window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) { + var sender = origAddTrack.apply(this, arguments); + if (!sender) { + sender = shimSenderWithDtmf(this, track); + this._senders.push(sender); + } + return sender; + }; + var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; + window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) { + origRemoveTrack.apply(this, arguments); + var idx = this._senders.indexOf(sender); + if (idx !== -1) { + this._senders.splice(idx, 1); + } + }; + } + var origAddStream = window.RTCPeerConnection.prototype.addStream; + window.RTCPeerConnection.prototype.addStream = function addStream(stream) { + var _this2 = this; + this._senders = this._senders || []; + origAddStream.apply(this, [stream]); + stream.getTracks().forEach(function (track) { + _this2._senders.push(shimSenderWithDtmf(_this2, track)); + }); + }; + var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; + window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { + var _this3 = this; + this._senders = this._senders || []; + origRemoveStream.apply(this, [stream]); + stream.getTracks().forEach(function (track) { + var sender = _this3._senders.find(function (s) { + return s.track === track; + }); + if (sender) { + // remove sender + _this3._senders.splice(_this3._senders.indexOf(sender), 1); + } + }); + }; + } else if (_typeof(window) === 'object' && window.RTCPeerConnection && 'getSenders' in window.RTCPeerConnection.prototype && 'createDTMFSender' in window.RTCPeerConnection.prototype && window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) { + var origGetSenders = window.RTCPeerConnection.prototype.getSenders; + window.RTCPeerConnection.prototype.getSenders = function getSenders() { + var _this4 = this; + var senders = origGetSenders.apply(this, []); + senders.forEach(function (sender) { + return sender._pc = _this4; + }); + return senders; + }; + Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { + get: function get() { + if (this._dtmf === undefined) { + if (this.track.kind === 'audio') { + this._dtmf = this._pc.createDTMFSender(this.track); + } else { + this._dtmf = null; + } + } + return this._dtmf; + } + }); + } +} +function shimGetStats(window) { + if (!window.RTCPeerConnection) { + return; + } + var origGetStats = window.RTCPeerConnection.prototype.getStats; + window.RTCPeerConnection.prototype.getStats = function getStats() { + var _this5 = this; + var _arguments = Array.prototype.slice.call(arguments), + selector = _arguments[0], + onSucc = _arguments[1], + onErr = _arguments[2]; + + // If selector is a function then we are in the old style stats so just + // pass back the original getStats format to avoid breaking old users. + if (arguments.length > 0 && typeof selector === 'function') { + return origGetStats.apply(this, arguments); + } + + // When spec-style getStats is supported, return those when called with + // either no arguments or the selector argument is null. + if (origGetStats.length === 0 && (arguments.length === 0 || typeof selector !== 'function')) { + return origGetStats.apply(this, []); + } + var fixChromeStats_ = function fixChromeStats_(response) { + var standardReport = {}; + var reports = response.result(); + reports.forEach(function (report) { + var standardStats = { + id: report.id, + timestamp: report.timestamp, + type: { + localcandidate: 'local-candidate', + remotecandidate: 'remote-candidate' + }[report.type] || report.type + }; + report.names().forEach(function (name) { + standardStats[name] = report.stat(name); + }); + standardReport[standardStats.id] = standardStats; + }); + return standardReport; + }; + + // shim getStats with maplike support + var makeMapStats = function makeMapStats(stats) { + return new Map(Object.keys(stats).map(function (key) { + return [key, stats[key]]; + })); + }; + if (arguments.length >= 2) { + var successCallbackWrapper_ = function successCallbackWrapper_(response) { + onSucc(makeMapStats(fixChromeStats_(response))); + }; + return origGetStats.apply(this, [successCallbackWrapper_, selector]); + } + + // promise-support + return new Promise(function (resolve, reject) { + origGetStats.apply(_this5, [function (response) { + resolve(makeMapStats(fixChromeStats_(response))); + }, reject]); + }).then(onSucc, onErr); + }; +} +function shimSenderReceiverGetStats(window) { + if (!(_typeof(window) === 'object' && window.RTCPeerConnection && window.RTCRtpSender && window.RTCRtpReceiver)) { + return; + } + + // shim sender stats. + if (!('getStats' in window.RTCRtpSender.prototype)) { + var origGetSenders = window.RTCPeerConnection.prototype.getSenders; + if (origGetSenders) { + window.RTCPeerConnection.prototype.getSenders = function getSenders() { + var _this6 = this; + var senders = origGetSenders.apply(this, []); + senders.forEach(function (sender) { + return sender._pc = _this6; + }); + return senders; + }; + } + var origAddTrack = window.RTCPeerConnection.prototype.addTrack; + if (origAddTrack) { + window.RTCPeerConnection.prototype.addTrack = function addTrack() { + var sender = origAddTrack.apply(this, arguments); + sender._pc = this; + return sender; + }; + } + window.RTCRtpSender.prototype.getStats = function getStats() { + var sender = this; + return this._pc.getStats().then(function (result) { + return ( + /* Note: this will include stats of all senders that + * send a track with the same id as sender.track as + * it is not possible to identify the RTCRtpSender. + */ + utils.filterStats(result, sender.track, true) + ); + }); + }; + } + + // shim receiver stats. + if (!('getStats' in window.RTCRtpReceiver.prototype)) { + var origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; + if (origGetReceivers) { + window.RTCPeerConnection.prototype.getReceivers = function getReceivers() { + var _this7 = this; + var receivers = origGetReceivers.apply(this, []); + receivers.forEach(function (receiver) { + return receiver._pc = _this7; + }); + return receivers; + }; + } + utils.wrapPeerConnectionEvent(window, 'track', function (e) { + e.receiver._pc = e.srcElement; + return e; + }); + window.RTCRtpReceiver.prototype.getStats = function getStats() { + var receiver = this; + return this._pc.getStats().then(function (result) { + return utils.filterStats(result, receiver.track, false); + }); + }; + } + if (!('getStats' in window.RTCRtpSender.prototype && 'getStats' in window.RTCRtpReceiver.prototype)) { + return; + } + + // shim RTCPeerConnection.getStats(track). + var origGetStats = window.RTCPeerConnection.prototype.getStats; + window.RTCPeerConnection.prototype.getStats = function getStats() { + if (arguments.length > 0 && arguments[0] instanceof window.MediaStreamTrack) { + var track = arguments[0]; + var sender; + var receiver; + var err; + this.getSenders().forEach(function (s) { + if (s.track === track) { + if (sender) { + err = true; + } else { + sender = s; + } + } + }); + this.getReceivers().forEach(function (r) { + if (r.track === track) { + if (receiver) { + err = true; + } else { + receiver = r; + } + } + return r.track === track; + }); + if (err || sender && receiver) { + return Promise.reject(new DOMException('There are more than one sender or receiver for the track.', 'InvalidAccessError')); + } else if (sender) { + return sender.getStats(); + } else if (receiver) { + return receiver.getStats(); + } + return Promise.reject(new DOMException('There is no sender or receiver for the track.', 'InvalidAccessError')); + } + return origGetStats.apply(this, arguments); + }; +} +function shimAddTrackRemoveTrackWithNative(window) { + // shim addTrack/removeTrack with native variants in order to make + // the interactions with legacy getLocalStreams behave as in other browsers. + // Keeps a mapping stream.id => [stream, rtpsenders...] + window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() { + var _this8 = this; + this._shimmedLocalStreams = this._shimmedLocalStreams || {}; + return Object.keys(this._shimmedLocalStreams).map(function (streamId) { + return _this8._shimmedLocalStreams[streamId][0]; + }); + }; + var origAddTrack = window.RTCPeerConnection.prototype.addTrack; + window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) { + if (!stream) { + return origAddTrack.apply(this, arguments); + } + this._shimmedLocalStreams = this._shimmedLocalStreams || {}; + var sender = origAddTrack.apply(this, arguments); + if (!this._shimmedLocalStreams[stream.id]) { + this._shimmedLocalStreams[stream.id] = [stream, sender]; + } else if (this._shimmedLocalStreams[stream.id].indexOf(sender) === -1) { + this._shimmedLocalStreams[stream.id].push(sender); + } + return sender; + }; + var origAddStream = window.RTCPeerConnection.prototype.addStream; + window.RTCPeerConnection.prototype.addStream = function addStream(stream) { + var _this9 = this; + this._shimmedLocalStreams = this._shimmedLocalStreams || {}; + stream.getTracks().forEach(function (track) { + var alreadyExists = _this9.getSenders().find(function (s) { + return s.track === track; + }); + if (alreadyExists) { + throw new DOMException('Track already exists.', 'InvalidAccessError'); + } + }); + var existingSenders = this.getSenders(); + origAddStream.apply(this, arguments); + var newSenders = this.getSenders().filter(function (newSender) { + return existingSenders.indexOf(newSender) === -1; + }); + this._shimmedLocalStreams[stream.id] = [stream].concat(newSenders); + }; + var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; + window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { + this._shimmedLocalStreams = this._shimmedLocalStreams || {}; + delete this._shimmedLocalStreams[stream.id]; + return origRemoveStream.apply(this, arguments); + }; + var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; + window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) { + var _this10 = this; + this._shimmedLocalStreams = this._shimmedLocalStreams || {}; + if (sender) { + Object.keys(this._shimmedLocalStreams).forEach(function (streamId) { + var idx = _this10._shimmedLocalStreams[streamId].indexOf(sender); + if (idx !== -1) { + _this10._shimmedLocalStreams[streamId].splice(idx, 1); + } + if (_this10._shimmedLocalStreams[streamId].length === 1) { + delete _this10._shimmedLocalStreams[streamId]; + } + }); + } + return origRemoveTrack.apply(this, arguments); + }; +} +function shimAddTrackRemoveTrack(window, browserDetails) { + if (!window.RTCPeerConnection) { + return; + } + // shim addTrack and removeTrack. + if (window.RTCPeerConnection.prototype.addTrack && browserDetails.version >= 65) { + return shimAddTrackRemoveTrackWithNative(window); + } + + // also shim pc.getLocalStreams when addTrack is shimmed + // to return the original streams. + var origGetLocalStreams = window.RTCPeerConnection.prototype.getLocalStreams; + window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() { + var _this11 = this; + var nativeStreams = origGetLocalStreams.apply(this); + this._reverseStreams = this._reverseStreams || {}; + return nativeStreams.map(function (stream) { + return _this11._reverseStreams[stream.id]; + }); + }; + var origAddStream = window.RTCPeerConnection.prototype.addStream; + window.RTCPeerConnection.prototype.addStream = function addStream(stream) { + var _this12 = this; + this._streams = this._streams || {}; + this._reverseStreams = this._reverseStreams || {}; + stream.getTracks().forEach(function (track) { + var alreadyExists = _this12.getSenders().find(function (s) { + return s.track === track; + }); + if (alreadyExists) { + throw new DOMException('Track already exists.', 'InvalidAccessError'); + } + }); + // Add identity mapping for consistency with addTrack. + // Unless this is being used with a stream from addTrack. + if (!this._reverseStreams[stream.id]) { + var newStream = new window.MediaStream(stream.getTracks()); + this._streams[stream.id] = newStream; + this._reverseStreams[newStream.id] = stream; + stream = newStream; + } + origAddStream.apply(this, [stream]); + }; + var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; + window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { + this._streams = this._streams || {}; + this._reverseStreams = this._reverseStreams || {}; + origRemoveStream.apply(this, [this._streams[stream.id] || stream]); + delete this._reverseStreams[this._streams[stream.id] ? this._streams[stream.id].id : stream.id]; + delete this._streams[stream.id]; + }; + window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) { + var _this13 = this; + if (this.signalingState === 'closed') { + throw new DOMException('The RTCPeerConnection\'s signalingState is \'closed\'.', 'InvalidStateError'); + } + var streams = [].slice.call(arguments, 1); + if (streams.length !== 1 || !streams[0].getTracks().find(function (t) { + return t === track; + })) { + // this is not fully correct but all we can manage without + // [[associated MediaStreams]] internal slot. + throw new DOMException('The adapter.js addTrack polyfill only supports a single ' + ' stream which is associated with the specified track.', 'NotSupportedError'); + } + var alreadyExists = this.getSenders().find(function (s) { + return s.track === track; + }); + if (alreadyExists) { + throw new DOMException('Track already exists.', 'InvalidAccessError'); + } + this._streams = this._streams || {}; + this._reverseStreams = this._reverseStreams || {}; + var oldStream = this._streams[stream.id]; + if (oldStream) { + // this is using odd Chrome behaviour, use with caution: + // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815 + // Note: we rely on the high-level addTrack/dtmf shim to + // create the sender with a dtmf sender. + oldStream.addTrack(track); + + // Trigger ONN async. + Promise.resolve().then(function () { + _this13.dispatchEvent(new Event('negotiationneeded')); + }); + } else { + var newStream = new window.MediaStream([track]); + this._streams[stream.id] = newStream; + this._reverseStreams[newStream.id] = stream; + this.addStream(newStream); + } + return this.getSenders().find(function (s) { + return s.track === track; + }); + }; + + // replace the internal stream id with the external one and + // vice versa. + function replaceInternalStreamId(pc, description) { + var sdp = description.sdp; + Object.keys(pc._reverseStreams || []).forEach(function (internalId) { + var externalStream = pc._reverseStreams[internalId]; + var internalStream = pc._streams[externalStream.id]; + sdp = sdp.replace(new RegExp(internalStream.id, 'g'), externalStream.id); + }); + return new RTCSessionDescription({ + type: description.type, + sdp: sdp + }); + } + function replaceExternalStreamId(pc, description) { + var sdp = description.sdp; + Object.keys(pc._reverseStreams || []).forEach(function (internalId) { + var externalStream = pc._reverseStreams[internalId]; + var internalStream = pc._streams[externalStream.id]; + sdp = sdp.replace(new RegExp(externalStream.id, 'g'), internalStream.id); + }); + return new RTCSessionDescription({ + type: description.type, + sdp: sdp + }); + } + ['createOffer', 'createAnswer'].forEach(function (method) { + var nativeMethod = window.RTCPeerConnection.prototype[method]; + var methodObj = _defineProperty({}, method, function () { + var _this14 = this; + var args = arguments; + var isLegacyCall = arguments.length && typeof arguments[0] === 'function'; + if (isLegacyCall) { + return nativeMethod.apply(this, [function (description) { + var desc = replaceInternalStreamId(_this14, description); + args[0].apply(null, [desc]); + }, function (err) { + if (args[1]) { + args[1].apply(null, err); + } + }, arguments[2]]); + } + return nativeMethod.apply(this, arguments).then(function (description) { + return replaceInternalStreamId(_this14, description); + }); + }); + window.RTCPeerConnection.prototype[method] = methodObj[method]; + }); + var origSetLocalDescription = window.RTCPeerConnection.prototype.setLocalDescription; + window.RTCPeerConnection.prototype.setLocalDescription = function setLocalDescription() { + if (!arguments.length || !arguments[0].type) { + return origSetLocalDescription.apply(this, arguments); + } + arguments[0] = replaceExternalStreamId(this, arguments[0]); + return origSetLocalDescription.apply(this, arguments); + }; + + // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier + + var origLocalDescription = Object.getOwnPropertyDescriptor(window.RTCPeerConnection.prototype, 'localDescription'); + Object.defineProperty(window.RTCPeerConnection.prototype, 'localDescription', { + get: function get() { + var description = origLocalDescription.get.apply(this); + if (description.type === '') { + return description; + } + return replaceInternalStreamId(this, description); + } + }); + window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) { + var _this15 = this; + if (this.signalingState === 'closed') { + throw new DOMException('The RTCPeerConnection\'s signalingState is \'closed\'.', 'InvalidStateError'); + } + // We can not yet check for sender instanceof RTCRtpSender + // since we shim RTPSender. So we check if sender._pc is set. + if (!sender._pc) { + throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' + 'does not implement interface RTCRtpSender.', 'TypeError'); + } + var isLocal = sender._pc === this; + if (!isLocal) { + throw new DOMException('Sender was not created by this connection.', 'InvalidAccessError'); + } + + // Search for the native stream the senders track belongs to. + this._streams = this._streams || {}; + var stream; + Object.keys(this._streams).forEach(function (streamid) { + var hasTrack = _this15._streams[streamid].getTracks().find(function (track) { + return sender.track === track; + }); + if (hasTrack) { + stream = _this15._streams[streamid]; + } + }); + if (stream) { + if (stream.getTracks().length === 1) { + // if this is the last track of the stream, remove the stream. This + // takes care of any shimmed _senders. + this.removeStream(this._reverseStreams[stream.id]); + } else { + // relying on the same odd chrome behaviour as above. + stream.removeTrack(sender.track); + } + this.dispatchEvent(new Event('negotiationneeded')); + } + }; +} +function shimPeerConnection(window, browserDetails) { + if (!window.RTCPeerConnection && window.webkitRTCPeerConnection) { + // very basic support for old versions. + window.RTCPeerConnection = window.webkitRTCPeerConnection; + } + if (!window.RTCPeerConnection) { + return; + } + + // shim implicit creation of RTCSessionDescription/RTCIceCandidate + if (browserDetails.version < 53) { + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'].forEach(function (method) { + var nativeMethod = window.RTCPeerConnection.prototype[method]; + var methodObj = _defineProperty({}, method, function () { + arguments[0] = new (method === 'addIceCandidate' ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]); + return nativeMethod.apply(this, arguments); + }); + window.RTCPeerConnection.prototype[method] = methodObj[method]; + }); + } +} + +// Attempt to fix ONN in plan-b mode. +function fixNegotiationNeeded(window, browserDetails) { + utils.wrapPeerConnectionEvent(window, 'negotiationneeded', function (e) { + var pc = e.target; + if (browserDetails.version < 72 || pc.getConfiguration && pc.getConfiguration().sdpSemantics === 'plan-b') { + if (pc.signalingState !== 'stable') { + return; + } + } + return e; + }); +} + +},{"../utils.js":11,"./getdisplaymedia":4,"./getusermedia":5}],4:[function(require,module,exports){ +/* + * Copyright (c) 2018 The adapter.js project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ +/* eslint-env node */ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.shimGetDisplayMedia = shimGetDisplayMedia; +function shimGetDisplayMedia(window, getSourceId) { + if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) { + return; + } + if (!window.navigator.mediaDevices) { + return; + } + // getSourceId is a function that returns a promise resolving with + // the sourceId of the screen/window/tab to be shared. + if (typeof getSourceId !== 'function') { + console.error('shimGetDisplayMedia: getSourceId argument is not ' + 'a function'); + return; + } + window.navigator.mediaDevices.getDisplayMedia = function getDisplayMedia(constraints) { + return getSourceId(constraints).then(function (sourceId) { + var widthSpecified = constraints.video && constraints.video.width; + var heightSpecified = constraints.video && constraints.video.height; + var frameRateSpecified = constraints.video && constraints.video.frameRate; + constraints.video = { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: sourceId, + maxFrameRate: frameRateSpecified || 3 + } + }; + if (widthSpecified) { + constraints.video.mandatory.maxWidth = widthSpecified; + } + if (heightSpecified) { + constraints.video.mandatory.maxHeight = heightSpecified; + } + return window.navigator.mediaDevices.getUserMedia(constraints); + }); + }; +} + +},{}],5:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ +/* eslint-env node */ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.shimGetUserMedia = shimGetUserMedia; +var utils = _interopRequireWildcard(require("../utils.js")); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } +var logging = utils.log; +function shimGetUserMedia(window, browserDetails) { + var navigator = window && window.navigator; + if (!navigator.mediaDevices) { + return; + } + var constraintsToChrome_ = function constraintsToChrome_(c) { + if (_typeof(c) !== 'object' || c.mandatory || c.optional) { + return c; + } + var cc = {}; + Object.keys(c).forEach(function (key) { + if (key === 'require' || key === 'advanced' || key === 'mediaSource') { + return; + } + var r = _typeof(c[key]) === 'object' ? c[key] : { + ideal: c[key] + }; + if (r.exact !== undefined && typeof r.exact === 'number') { + r.min = r.max = r.exact; + } + var oldname_ = function oldname_(prefix, name) { + if (prefix) { + return prefix + name.charAt(0).toUpperCase() + name.slice(1); + } + return name === 'deviceId' ? 'sourceId' : name; + }; + if (r.ideal !== undefined) { + cc.optional = cc.optional || []; + var oc = {}; + if (typeof r.ideal === 'number') { + oc[oldname_('min', key)] = r.ideal; + cc.optional.push(oc); + oc = {}; + oc[oldname_('max', key)] = r.ideal; + cc.optional.push(oc); + } else { + oc[oldname_('', key)] = r.ideal; + cc.optional.push(oc); + } + } + if (r.exact !== undefined && typeof r.exact !== 'number') { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname_('', key)] = r.exact; + } else { + ['min', 'max'].forEach(function (mix) { + if (r[mix] !== undefined) { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname_(mix, key)] = r[mix]; + } + }); + } + }); + if (c.advanced) { + cc.optional = (cc.optional || []).concat(c.advanced); + } + return cc; + }; + var shimConstraints_ = function shimConstraints_(constraints, func) { + if (browserDetails.version >= 61) { + return func(constraints); + } + constraints = JSON.parse(JSON.stringify(constraints)); + if (constraints && _typeof(constraints.audio) === 'object') { + var remap = function remap(obj, a, b) { + if (a in obj && !(b in obj)) { + obj[b] = obj[a]; + delete obj[a]; + } + }; + constraints = JSON.parse(JSON.stringify(constraints)); + remap(constraints.audio, 'autoGainControl', 'googAutoGainControl'); + remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression'); + constraints.audio = constraintsToChrome_(constraints.audio); + } + if (constraints && _typeof(constraints.video) === 'object') { + // Shim facingMode for mobile & surface pro. + var face = constraints.video.facingMode; + face = face && (_typeof(face) === 'object' ? face : { + ideal: face + }); + var getSupportedFacingModeLies = browserDetails.version < 66; + if (face && (face.exact === 'user' || face.exact === 'environment' || face.ideal === 'user' || face.ideal === 'environment') && !(navigator.mediaDevices.getSupportedConstraints && navigator.mediaDevices.getSupportedConstraints().facingMode && !getSupportedFacingModeLies)) { + delete constraints.video.facingMode; + var matches; + if (face.exact === 'environment' || face.ideal === 'environment') { + matches = ['back', 'rear']; + } else if (face.exact === 'user' || face.ideal === 'user') { + matches = ['front']; + } + if (matches) { + // Look for matches in label, or use last cam for back (typical). + return navigator.mediaDevices.enumerateDevices().then(function (devices) { + devices = devices.filter(function (d) { + return d.kind === 'videoinput'; + }); + var dev = devices.find(function (d) { + return matches.some(function (match) { + return d.label.toLowerCase().includes(match); + }); + }); + if (!dev && devices.length && matches.includes('back')) { + dev = devices[devices.length - 1]; // more likely the back cam + } + + if (dev) { + constraints.video.deviceId = face.exact ? { + exact: dev.deviceId + } : { + ideal: dev.deviceId + }; + } + constraints.video = constraintsToChrome_(constraints.video); + logging('chrome: ' + JSON.stringify(constraints)); + return func(constraints); + }); + } + } + constraints.video = constraintsToChrome_(constraints.video); + } + logging('chrome: ' + JSON.stringify(constraints)); + return func(constraints); + }; + var shimError_ = function shimError_(e) { + if (browserDetails.version >= 64) { + return e; + } + return { + name: { + PermissionDeniedError: 'NotAllowedError', + PermissionDismissedError: 'NotAllowedError', + InvalidStateError: 'NotAllowedError', + DevicesNotFoundError: 'NotFoundError', + ConstraintNotSatisfiedError: 'OverconstrainedError', + TrackStartError: 'NotReadableError', + MediaDeviceFailedDueToShutdown: 'NotAllowedError', + MediaDeviceKillSwitchOn: 'NotAllowedError', + TabCaptureError: 'AbortError', + ScreenCaptureError: 'AbortError', + DeviceCaptureError: 'AbortError' + }[e.name] || e.name, + message: e.message, + constraint: e.constraint || e.constraintName, + toString: function toString() { + return this.name + (this.message && ': ') + this.message; + } + }; + }; + var getUserMedia_ = function getUserMedia_(constraints, onSuccess, onError) { + shimConstraints_(constraints, function (c) { + navigator.webkitGetUserMedia(c, onSuccess, function (e) { + if (onError) { + onError(shimError_(e)); + } + }); + }); + }; + navigator.getUserMedia = getUserMedia_.bind(navigator); + + // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia + // function which returns a Promise, it does not accept spec-style + // constraints. + if (navigator.mediaDevices.getUserMedia) { + var origGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function (cs) { + return shimConstraints_(cs, function (c) { + return origGetUserMedia(c).then(function (stream) { + if (c.audio && !stream.getAudioTracks().length || c.video && !stream.getVideoTracks().length) { + stream.getTracks().forEach(function (track) { + track.stop(); + }); + throw new DOMException('', 'NotFoundError'); + } + return stream; + }, function (e) { + return Promise.reject(shimError_(e)); + }); + }); + }; + } +} + +},{"../utils.js":11}],6:[function(require,module,exports){ +/* + * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ +/* eslint-env node */ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.removeExtmapAllowMixed = removeExtmapAllowMixed; +exports.shimAddIceCandidateNullOrEmpty = shimAddIceCandidateNullOrEmpty; +exports.shimConnectionState = shimConnectionState; +exports.shimMaxMessageSize = shimMaxMessageSize; +exports.shimParameterlessSetLocalDescription = shimParameterlessSetLocalDescription; +exports.shimRTCIceCandidate = shimRTCIceCandidate; +exports.shimRTCIceCandidateRelayProtocol = shimRTCIceCandidateRelayProtocol; +exports.shimSendThrowTypeError = shimSendThrowTypeError; +var _sdp = _interopRequireDefault(require("sdp")); +var utils = _interopRequireWildcard(require("./utils")); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } +function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } +function shimRTCIceCandidate(window) { + // foundation is arbitrarily chosen as an indicator for full support for + // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface + if (!window.RTCIceCandidate || window.RTCIceCandidate && 'foundation' in window.RTCIceCandidate.prototype) { + return; + } + var NativeRTCIceCandidate = window.RTCIceCandidate; + window.RTCIceCandidate = function RTCIceCandidate(args) { + // Remove the a= which shouldn't be part of the candidate string. + if (_typeof(args) === 'object' && args.candidate && args.candidate.indexOf('a=') === 0) { + args = JSON.parse(JSON.stringify(args)); + args.candidate = args.candidate.substring(2); + } + if (args.candidate && args.candidate.length) { + // Augment the native candidate with the parsed fields. + var nativeCandidate = new NativeRTCIceCandidate(args); + var parsedCandidate = _sdp["default"].parseCandidate(args.candidate); + for (var key in parsedCandidate) { + if (!(key in nativeCandidate)) { + Object.defineProperty(nativeCandidate, key, { + value: parsedCandidate[key] + }); + } + } + + // Override serializer to not serialize the extra attributes. + nativeCandidate.toJSON = function toJSON() { + return { + candidate: nativeCandidate.candidate, + sdpMid: nativeCandidate.sdpMid, + sdpMLineIndex: nativeCandidate.sdpMLineIndex, + usernameFragment: nativeCandidate.usernameFragment + }; + }; + return nativeCandidate; + } + return new NativeRTCIceCandidate(args); + }; + window.RTCIceCandidate.prototype = NativeRTCIceCandidate.prototype; + + // Hook up the augmented candidate in onicecandidate and + // addEventListener('icecandidate', ...) + utils.wrapPeerConnectionEvent(window, 'icecandidate', function (e) { + if (e.candidate) { + Object.defineProperty(e, 'candidate', { + value: new window.RTCIceCandidate(e.candidate), + writable: 'false' + }); + } + return e; + }); +} +function shimRTCIceCandidateRelayProtocol(window) { + if (!window.RTCIceCandidate || window.RTCIceCandidate && 'relayProtocol' in window.RTCIceCandidate.prototype) { + return; + } + + // Hook up the augmented candidate in onicecandidate and + // addEventListener('icecandidate', ...) + utils.wrapPeerConnectionEvent(window, 'icecandidate', function (e) { + if (e.candidate) { + var parsedCandidate = _sdp["default"].parseCandidate(e.candidate.candidate); + if (parsedCandidate.type === 'relay') { + // This is a libwebrtc-specific mapping of local type preference + // to relayProtocol. + e.candidate.relayProtocol = { + 0: 'tls', + 1: 'tcp', + 2: 'udp' + }[parsedCandidate.priority >> 24]; + } + } + return e; + }); +} +function shimMaxMessageSize(window, browserDetails) { + if (!window.RTCPeerConnection) { + return; + } + if (!('sctp' in window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'sctp', { + get: function get() { + return typeof this._sctp === 'undefined' ? null : this._sctp; + } + }); + } + var sctpInDescription = function sctpInDescription(description) { + if (!description || !description.sdp) { + return false; + } + var sections = _sdp["default"].splitSections(description.sdp); + sections.shift(); + return sections.some(function (mediaSection) { + var mLine = _sdp["default"].parseMLine(mediaSection); + return mLine && mLine.kind === 'application' && mLine.protocol.indexOf('SCTP') !== -1; + }); + }; + var getRemoteFirefoxVersion = function getRemoteFirefoxVersion(description) { + // TODO: Is there a better solution for detecting Firefox? + var match = description.sdp.match(/mozilla...THIS_IS_SDPARTA-(\d+)/); + if (match === null || match.length < 2) { + return -1; + } + var version = parseInt(match[1], 10); + // Test for NaN (yes, this is ugly) + return version !== version ? -1 : version; + }; + var getCanSendMaxMessageSize = function getCanSendMaxMessageSize(remoteIsFirefox) { + // Every implementation we know can send at least 64 KiB. + // Note: Although Chrome is technically able to send up to 256 KiB, the + // data does not reach the other peer reliably. + // See: https://bugs.chromium.org/p/webrtc/issues/detail?id=8419 + var canSendMaxMessageSize = 65536; + if (browserDetails.browser === 'firefox') { + if (browserDetails.version < 57) { + if (remoteIsFirefox === -1) { + // FF < 57 will send in 16 KiB chunks using the deprecated PPID + // fragmentation. + canSendMaxMessageSize = 16384; + } else { + // However, other FF (and RAWRTC) can reassemble PPID-fragmented + // messages. Thus, supporting ~2 GiB when sending. + canSendMaxMessageSize = 2147483637; + } + } else if (browserDetails.version < 60) { + // Currently, all FF >= 57 will reset the remote maximum message size + // to the default value when a data channel is created at a later + // stage. :( + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 + canSendMaxMessageSize = browserDetails.version === 57 ? 65535 : 65536; + } else { + // FF >= 60 supports sending ~2 GiB + canSendMaxMessageSize = 2147483637; + } + } + return canSendMaxMessageSize; + }; + var getMaxMessageSize = function getMaxMessageSize(description, remoteIsFirefox) { + // Note: 65536 bytes is the default value from the SDP spec. Also, + // every implementation we know supports receiving 65536 bytes. + var maxMessageSize = 65536; + + // FF 57 has a slightly incorrect default remote max message size, so + // we need to adjust it here to avoid a failure when sending. + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1425697 + if (browserDetails.browser === 'firefox' && browserDetails.version === 57) { + maxMessageSize = 65535; + } + var match = _sdp["default"].matchPrefix(description.sdp, 'a=max-message-size:'); + if (match.length > 0) { + maxMessageSize = parseInt(match[0].substring(19), 10); + } else if (browserDetails.browser === 'firefox' && remoteIsFirefox !== -1) { + // If the maximum message size is not present in the remote SDP and + // both local and remote are Firefox, the remote peer can receive + // ~2 GiB. + maxMessageSize = 2147483637; + } + return maxMessageSize; + }; + var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; + window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() { + this._sctp = null; + // Chrome decided to not expose .sctp in plan-b mode. + // As usual, adapter.js has to do an 'ugly worakaround' + // to cover up the mess. + if (browserDetails.browser === 'chrome' && browserDetails.version >= 76) { + var _this$getConfiguratio = this.getConfiguration(), + sdpSemantics = _this$getConfiguratio.sdpSemantics; + if (sdpSemantics === 'plan-b') { + Object.defineProperty(this, 'sctp', { + get: function get() { + return typeof this._sctp === 'undefined' ? null : this._sctp; + }, + enumerable: true, + configurable: true + }); + } + } + if (sctpInDescription(arguments[0])) { + // Check if the remote is FF. + var isFirefox = getRemoteFirefoxVersion(arguments[0]); + + // Get the maximum message size the local peer is capable of sending + var canSendMMS = getCanSendMaxMessageSize(isFirefox); + + // Get the maximum message size of the remote peer. + var remoteMMS = getMaxMessageSize(arguments[0], isFirefox); + + // Determine final maximum message size + var maxMessageSize; + if (canSendMMS === 0 && remoteMMS === 0) { + maxMessageSize = Number.POSITIVE_INFINITY; + } else if (canSendMMS === 0 || remoteMMS === 0) { + maxMessageSize = Math.max(canSendMMS, remoteMMS); + } else { + maxMessageSize = Math.min(canSendMMS, remoteMMS); + } + + // Create a dummy RTCSctpTransport object and the 'maxMessageSize' + // attribute. + var sctp = {}; + Object.defineProperty(sctp, 'maxMessageSize', { + get: function get() { + return maxMessageSize; + } + }); + this._sctp = sctp; + } + return origSetRemoteDescription.apply(this, arguments); + }; +} +function shimSendThrowTypeError(window) { + if (!(window.RTCPeerConnection && 'createDataChannel' in window.RTCPeerConnection.prototype)) { + return; + } + + // Note: Although Firefox >= 57 has a native implementation, the maximum + // message size can be reset for all data channels at a later stage. + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 + + function wrapDcSend(dc, pc) { + var origDataChannelSend = dc.send; + dc.send = function send() { + var data = arguments[0]; + var length = data.length || data.size || data.byteLength; + if (dc.readyState === 'open' && pc.sctp && length > pc.sctp.maxMessageSize) { + throw new TypeError('Message too large (can send a maximum of ' + pc.sctp.maxMessageSize + ' bytes)'); + } + return origDataChannelSend.apply(dc, arguments); + }; + } + var origCreateDataChannel = window.RTCPeerConnection.prototype.createDataChannel; + window.RTCPeerConnection.prototype.createDataChannel = function createDataChannel() { + var dataChannel = origCreateDataChannel.apply(this, arguments); + wrapDcSend(dataChannel, this); + return dataChannel; + }; + utils.wrapPeerConnectionEvent(window, 'datachannel', function (e) { + wrapDcSend(e.channel, e.target); + return e; + }); +} + +/* shims RTCConnectionState by pretending it is the same as iceConnectionState. + * See https://bugs.chromium.org/p/webrtc/issues/detail?id=6145#c12 + * for why this is a valid hack in Chrome. In Firefox it is slightly incorrect + * since DTLS failures would be hidden. See + * https://bugzilla.mozilla.org/show_bug.cgi?id=1265827 + * for the Firefox tracking bug. + */ +function shimConnectionState(window) { + if (!window.RTCPeerConnection || 'connectionState' in window.RTCPeerConnection.prototype) { + return; + } + var proto = window.RTCPeerConnection.prototype; + Object.defineProperty(proto, 'connectionState', { + get: function get() { + return { + completed: 'connected', + checking: 'connecting' + }[this.iceConnectionState] || this.iceConnectionState; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(proto, 'onconnectionstatechange', { + get: function get() { + return this._onconnectionstatechange || null; + }, + set: function set(cb) { + if (this._onconnectionstatechange) { + this.removeEventListener('connectionstatechange', this._onconnectionstatechange); + delete this._onconnectionstatechange; + } + if (cb) { + this.addEventListener('connectionstatechange', this._onconnectionstatechange = cb); + } + }, + enumerable: true, + configurable: true + }); + ['setLocalDescription', 'setRemoteDescription'].forEach(function (method) { + var origMethod = proto[method]; + proto[method] = function () { + if (!this._connectionstatechangepoly) { + this._connectionstatechangepoly = function (e) { + var pc = e.target; + if (pc._lastConnectionState !== pc.connectionState) { + pc._lastConnectionState = pc.connectionState; + var newEvent = new Event('connectionstatechange', e); + pc.dispatchEvent(newEvent); + } + return e; + }; + this.addEventListener('iceconnectionstatechange', this._connectionstatechangepoly); + } + return origMethod.apply(this, arguments); + }; + }); +} +function removeExtmapAllowMixed(window, browserDetails) { + /* remove a=extmap-allow-mixed for webrtc.org < M71 */ + if (!window.RTCPeerConnection) { + return; + } + if (browserDetails.browser === 'chrome' && browserDetails.version >= 71) { + return; + } + if (browserDetails.browser === 'safari' && browserDetails.version >= 605) { + return; + } + var nativeSRD = window.RTCPeerConnection.prototype.setRemoteDescription; + window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription(desc) { + if (desc && desc.sdp && desc.sdp.indexOf('\na=extmap-allow-mixed') !== -1) { + var sdp = desc.sdp.split('\n').filter(function (line) { + return line.trim() !== 'a=extmap-allow-mixed'; + }).join('\n'); + // Safari enforces read-only-ness of RTCSessionDescription fields. + if (window.RTCSessionDescription && desc instanceof window.RTCSessionDescription) { + arguments[0] = new window.RTCSessionDescription({ + type: desc.type, + sdp: sdp + }); + } else { + desc.sdp = sdp; + } + } + return nativeSRD.apply(this, arguments); + }; +} +function shimAddIceCandidateNullOrEmpty(window, browserDetails) { + // Support for addIceCandidate(null or undefined) + // as well as addIceCandidate({candidate: "", ...}) + // https://bugs.chromium.org/p/chromium/issues/detail?id=978582 + // Note: must be called before other polyfills which change the signature. + if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) { + return; + } + var nativeAddIceCandidate = window.RTCPeerConnection.prototype.addIceCandidate; + if (!nativeAddIceCandidate || nativeAddIceCandidate.length === 0) { + return; + } + window.RTCPeerConnection.prototype.addIceCandidate = function addIceCandidate() { + if (!arguments[0]) { + if (arguments[1]) { + arguments[1].apply(null); + } + return Promise.resolve(); + } + // Firefox 68+ emits and processes {candidate: "", ...}, ignore + // in older versions. + // Native support for ignoring exists for Chrome M77+. + // Safari ignores as well, exact version unknown but works in the same + // version that also ignores addIceCandidate(null). + if ((browserDetails.browser === 'chrome' && browserDetails.version < 78 || browserDetails.browser === 'firefox' && browserDetails.version < 68 || browserDetails.browser === 'safari') && arguments[0] && arguments[0].candidate === '') { + return Promise.resolve(); + } + return nativeAddIceCandidate.apply(this, arguments); + }; +} + +// Note: Make sure to call this ahead of APIs that modify +// setLocalDescription.length +function shimParameterlessSetLocalDescription(window, browserDetails) { + if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) { + return; + } + var nativeSetLocalDescription = window.RTCPeerConnection.prototype.setLocalDescription; + if (!nativeSetLocalDescription || nativeSetLocalDescription.length === 0) { + return; + } + window.RTCPeerConnection.prototype.setLocalDescription = function setLocalDescription() { + var _this = this; + var desc = arguments[0] || {}; + if (_typeof(desc) !== 'object' || desc.type && desc.sdp) { + return nativeSetLocalDescription.apply(this, arguments); + } + // The remaining steps should technically happen when SLD comes off the + // RTCPeerConnection's operations chain (not ahead of going on it), but + // this is too difficult to shim. Instead, this shim only covers the + // common case where the operations chain is empty. This is imperfect, but + // should cover many cases. Rationale: Even if we can't reduce the glare + // window to zero on imperfect implementations, there's value in tapping + // into the perfect negotiation pattern that several browsers support. + desc = { + type: desc.type, + sdp: desc.sdp + }; + if (!desc.type) { + switch (this.signalingState) { + case 'stable': + case 'have-local-offer': + case 'have-remote-pranswer': + desc.type = 'offer'; + break; + default: + desc.type = 'answer'; + break; + } + } + if (desc.sdp || desc.type !== 'offer' && desc.type !== 'answer') { + return nativeSetLocalDescription.apply(this, [desc]); + } + var func = desc.type === 'offer' ? this.createOffer : this.createAnswer; + return func.apply(this).then(function (d) { + return nativeSetLocalDescription.apply(_this, [d]); + }); + }; +} + +},{"./utils":11,"sdp":12}],7:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ +/* eslint-env node */ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.shimAddTransceiver = shimAddTransceiver; +exports.shimCreateAnswer = shimCreateAnswer; +exports.shimCreateOffer = shimCreateOffer; +Object.defineProperty(exports, "shimGetDisplayMedia", { + enumerable: true, + get: function get() { + return _getdisplaymedia.shimGetDisplayMedia; + } +}); +exports.shimGetParameters = shimGetParameters; +Object.defineProperty(exports, "shimGetUserMedia", { + enumerable: true, + get: function get() { + return _getusermedia.shimGetUserMedia; + } +}); +exports.shimOnTrack = shimOnTrack; +exports.shimPeerConnection = shimPeerConnection; +exports.shimRTCDataChannel = shimRTCDataChannel; +exports.shimReceiverGetStats = shimReceiverGetStats; +exports.shimRemoveStream = shimRemoveStream; +exports.shimSenderGetStats = shimSenderGetStats; +var utils = _interopRequireWildcard(require("../utils")); +var _getusermedia = require("./getusermedia"); +var _getdisplaymedia = require("./getdisplaymedia"); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } +function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } +function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } +function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } +function shimOnTrack(window) { + if (_typeof(window) === 'object' && window.RTCTrackEvent && 'receiver' in window.RTCTrackEvent.prototype && !('transceiver' in window.RTCTrackEvent.prototype)) { + Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { + get: function get() { + return { + receiver: this.receiver + }; + } + }); + } +} +function shimPeerConnection(window, browserDetails) { + if (_typeof(window) !== 'object' || !(window.RTCPeerConnection || window.mozRTCPeerConnection)) { + return; // probably media.peerconnection.enabled=false in about:config + } + + if (!window.RTCPeerConnection && window.mozRTCPeerConnection) { + // very basic support for old versions. + window.RTCPeerConnection = window.mozRTCPeerConnection; + } + if (browserDetails.version < 53) { + // shim away need for obsolete RTCIceCandidate/RTCSessionDescription. + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'].forEach(function (method) { + var nativeMethod = window.RTCPeerConnection.prototype[method]; + var methodObj = _defineProperty({}, method, function () { + arguments[0] = new (method === 'addIceCandidate' ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]); + return nativeMethod.apply(this, arguments); + }); + window.RTCPeerConnection.prototype[method] = methodObj[method]; + }); + } + var modernStatsTypes = { + inboundrtp: 'inbound-rtp', + outboundrtp: 'outbound-rtp', + candidatepair: 'candidate-pair', + localcandidate: 'local-candidate', + remotecandidate: 'remote-candidate' + }; + var nativeGetStats = window.RTCPeerConnection.prototype.getStats; + window.RTCPeerConnection.prototype.getStats = function getStats() { + var _arguments = Array.prototype.slice.call(arguments), + selector = _arguments[0], + onSucc = _arguments[1], + onErr = _arguments[2]; + return nativeGetStats.apply(this, [selector || null]).then(function (stats) { + if (browserDetails.version < 53 && !onSucc) { + // Shim only promise getStats with spec-hyphens in type names + // Leave callback version alone; misc old uses of forEach before Map + try { + stats.forEach(function (stat) { + stat.type = modernStatsTypes[stat.type] || stat.type; + }); + } catch (e) { + if (e.name !== 'TypeError') { + throw e; + } + // Avoid TypeError: "type" is read-only, in old versions. 34-43ish + stats.forEach(function (stat, i) { + stats.set(i, Object.assign({}, stat, { + type: modernStatsTypes[stat.type] || stat.type + })); + }); + } + } + return stats; + }).then(onSucc, onErr); + }; +} +function shimSenderGetStats(window) { + if (!(_typeof(window) === 'object' && window.RTCPeerConnection && window.RTCRtpSender)) { + return; + } + if (window.RTCRtpSender && 'getStats' in window.RTCRtpSender.prototype) { + return; + } + var origGetSenders = window.RTCPeerConnection.prototype.getSenders; + if (origGetSenders) { + window.RTCPeerConnection.prototype.getSenders = function getSenders() { + var _this = this; + var senders = origGetSenders.apply(this, []); + senders.forEach(function (sender) { + return sender._pc = _this; + }); + return senders; + }; + } + var origAddTrack = window.RTCPeerConnection.prototype.addTrack; + if (origAddTrack) { + window.RTCPeerConnection.prototype.addTrack = function addTrack() { + var sender = origAddTrack.apply(this, arguments); + sender._pc = this; + return sender; + }; + } + window.RTCRtpSender.prototype.getStats = function getStats() { + return this.track ? this._pc.getStats(this.track) : Promise.resolve(new Map()); + }; +} +function shimReceiverGetStats(window) { + if (!(_typeof(window) === 'object' && window.RTCPeerConnection && window.RTCRtpSender)) { + return; + } + if (window.RTCRtpSender && 'getStats' in window.RTCRtpReceiver.prototype) { + return; + } + var origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; + if (origGetReceivers) { + window.RTCPeerConnection.prototype.getReceivers = function getReceivers() { + var _this2 = this; + var receivers = origGetReceivers.apply(this, []); + receivers.forEach(function (receiver) { + return receiver._pc = _this2; + }); + return receivers; + }; + } + utils.wrapPeerConnectionEvent(window, 'track', function (e) { + e.receiver._pc = e.srcElement; + return e; + }); + window.RTCRtpReceiver.prototype.getStats = function getStats() { + return this._pc.getStats(this.track); + }; +} +function shimRemoveStream(window) { + if (!window.RTCPeerConnection || 'removeStream' in window.RTCPeerConnection.prototype) { + return; + } + window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { + var _this3 = this; + utils.deprecated('removeStream', 'removeTrack'); + this.getSenders().forEach(function (sender) { + if (sender.track && stream.getTracks().includes(sender.track)) { + _this3.removeTrack(sender); + } + }); + }; +} +function shimRTCDataChannel(window) { + // rename DataChannel to RTCDataChannel (native fix in FF60): + // https://bugzilla.mozilla.org/show_bug.cgi?id=1173851 + if (window.DataChannel && !window.RTCDataChannel) { + window.RTCDataChannel = window.DataChannel; + } +} +function shimAddTransceiver(window) { + // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 + // Firefox ignores the init sendEncodings options passed to addTransceiver + // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 + if (!(_typeof(window) === 'object' && window.RTCPeerConnection)) { + return; + } + var origAddTransceiver = window.RTCPeerConnection.prototype.addTransceiver; + if (origAddTransceiver) { + window.RTCPeerConnection.prototype.addTransceiver = function addTransceiver() { + this.setParametersPromises = []; + // WebIDL input coercion and validation + var sendEncodings = arguments[1] && arguments[1].sendEncodings; + if (sendEncodings === undefined) { + sendEncodings = []; + } + sendEncodings = _toConsumableArray(sendEncodings); + var shouldPerformCheck = sendEncodings.length > 0; + if (shouldPerformCheck) { + // If sendEncodings params are provided, validate grammar + sendEncodings.forEach(function (encodingParam) { + if ('rid' in encodingParam) { + var ridRegex = /^[a-z0-9]{0,16}$/i; + if (!ridRegex.test(encodingParam.rid)) { + throw new TypeError('Invalid RID value provided.'); + } + } + if ('scaleResolutionDownBy' in encodingParam) { + if (!(parseFloat(encodingParam.scaleResolutionDownBy) >= 1.0)) { + throw new RangeError('scale_resolution_down_by must be >= 1.0'); + } + } + if ('maxFramerate' in encodingParam) { + if (!(parseFloat(encodingParam.maxFramerate) >= 0)) { + throw new RangeError('max_framerate must be >= 0.0'); + } + } + }); + } + var transceiver = origAddTransceiver.apply(this, arguments); + if (shouldPerformCheck) { + // Check if the init options were applied. If not we do this in an + // asynchronous way and save the promise reference in a global object. + // This is an ugly hack, but at the same time is way more robust than + // checking the sender parameters before and after the createOffer + // Also note that after the createoffer we are not 100% sure that + // the params were asynchronously applied so we might miss the + // opportunity to recreate offer. + var sender = transceiver.sender; + var params = sender.getParameters(); + if (!('encodings' in params) || + // Avoid being fooled by patched getParameters() below. + params.encodings.length === 1 && Object.keys(params.encodings[0]).length === 0) { + params.encodings = sendEncodings; + sender.sendEncodings = sendEncodings; + this.setParametersPromises.push(sender.setParameters(params).then(function () { + delete sender.sendEncodings; + })["catch"](function () { + delete sender.sendEncodings; + })); + } + } + return transceiver; + }; + } +} +function shimGetParameters(window) { + if (!(_typeof(window) === 'object' && window.RTCRtpSender)) { + return; + } + var origGetParameters = window.RTCRtpSender.prototype.getParameters; + if (origGetParameters) { + window.RTCRtpSender.prototype.getParameters = function getParameters() { + var params = origGetParameters.apply(this, arguments); + if (!('encodings' in params)) { + params.encodings = [].concat(this.sendEncodings || [{}]); + } + return params; + }; + } +} +function shimCreateOffer(window) { + // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 + // Firefox ignores the init sendEncodings options passed to addTransceiver + // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 + if (!(_typeof(window) === 'object' && window.RTCPeerConnection)) { + return; + } + var origCreateOffer = window.RTCPeerConnection.prototype.createOffer; + window.RTCPeerConnection.prototype.createOffer = function createOffer() { + var _arguments2 = arguments, + _this4 = this; + if (this.setParametersPromises && this.setParametersPromises.length) { + return Promise.all(this.setParametersPromises).then(function () { + return origCreateOffer.apply(_this4, _arguments2); + })["finally"](function () { + _this4.setParametersPromises = []; + }); + } + return origCreateOffer.apply(this, arguments); + }; +} +function shimCreateAnswer(window) { + // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 + // Firefox ignores the init sendEncodings options passed to addTransceiver + // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 + if (!(_typeof(window) === 'object' && window.RTCPeerConnection)) { + return; + } + var origCreateAnswer = window.RTCPeerConnection.prototype.createAnswer; + window.RTCPeerConnection.prototype.createAnswer = function createAnswer() { + var _arguments3 = arguments, + _this5 = this; + if (this.setParametersPromises && this.setParametersPromises.length) { + return Promise.all(this.setParametersPromises).then(function () { + return origCreateAnswer.apply(_this5, _arguments3); + })["finally"](function () { + _this5.setParametersPromises = []; + }); + } + return origCreateAnswer.apply(this, arguments); + }; +} + +},{"../utils":11,"./getdisplaymedia":8,"./getusermedia":9}],8:[function(require,module,exports){ +/* + * Copyright (c) 2018 The adapter.js project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ +/* eslint-env node */ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.shimGetDisplayMedia = shimGetDisplayMedia; +function shimGetDisplayMedia(window, preferredMediaSource) { + if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) { + return; + } + if (!window.navigator.mediaDevices) { + return; + } + window.navigator.mediaDevices.getDisplayMedia = function getDisplayMedia(constraints) { + if (!(constraints && constraints.video)) { + var err = new DOMException('getDisplayMedia without video ' + 'constraints is undefined'); + err.name = 'NotFoundError'; + // from https://heycam.github.io/webidl/#idl-DOMException-error-names + err.code = 8; + return Promise.reject(err); + } + if (constraints.video === true) { + constraints.video = { + mediaSource: preferredMediaSource + }; + } else { + constraints.video.mediaSource = preferredMediaSource; + } + return window.navigator.mediaDevices.getUserMedia(constraints); + }; +} + +},{}],9:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ +/* eslint-env node */ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.shimGetUserMedia = shimGetUserMedia; +var utils = _interopRequireWildcard(require("../utils")); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } +function shimGetUserMedia(window, browserDetails) { + var navigator = window && window.navigator; + var MediaStreamTrack = window && window.MediaStreamTrack; + navigator.getUserMedia = function (constraints, onSuccess, onError) { + // Replace Firefox 44+'s deprecation warning with unprefixed version. + utils.deprecated('navigator.getUserMedia', 'navigator.mediaDevices.getUserMedia'); + navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError); + }; + if (!(browserDetails.version > 55 && 'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) { + var remap = function remap(obj, a, b) { + if (a in obj && !(b in obj)) { + obj[b] = obj[a]; + delete obj[a]; + } + }; + var nativeGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function (c) { + if (_typeof(c) === 'object' && _typeof(c.audio) === 'object') { + c = JSON.parse(JSON.stringify(c)); + remap(c.audio, 'autoGainControl', 'mozAutoGainControl'); + remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression'); + } + return nativeGetUserMedia(c); + }; + if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) { + var nativeGetSettings = MediaStreamTrack.prototype.getSettings; + MediaStreamTrack.prototype.getSettings = function () { + var obj = nativeGetSettings.apply(this, arguments); + remap(obj, 'mozAutoGainControl', 'autoGainControl'); + remap(obj, 'mozNoiseSuppression', 'noiseSuppression'); + return obj; + }; + } + if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) { + var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints; + MediaStreamTrack.prototype.applyConstraints = function (c) { + if (this.kind === 'audio' && _typeof(c) === 'object') { + c = JSON.parse(JSON.stringify(c)); + remap(c, 'autoGainControl', 'mozAutoGainControl'); + remap(c, 'noiseSuppression', 'mozNoiseSuppression'); + } + return nativeApplyConstraints.apply(this, [c]); + }; + } + } +} + +},{"../utils":11}],10:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.shimAudioContext = shimAudioContext; +exports.shimCallbacksAPI = shimCallbacksAPI; +exports.shimConstraints = shimConstraints; +exports.shimCreateOfferLegacy = shimCreateOfferLegacy; +exports.shimGetUserMedia = shimGetUserMedia; +exports.shimLocalStreamsAPI = shimLocalStreamsAPI; +exports.shimRTCIceServerUrls = shimRTCIceServerUrls; +exports.shimRemoteStreamsAPI = shimRemoteStreamsAPI; +exports.shimTrackEventTransceiver = shimTrackEventTransceiver; +var utils = _interopRequireWildcard(require("../utils")); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } +function shimLocalStreamsAPI(window) { + if (_typeof(window) !== 'object' || !window.RTCPeerConnection) { + return; + } + if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) { + window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() { + if (!this._localStreams) { + this._localStreams = []; + } + return this._localStreams; + }; + } + if (!('addStream' in window.RTCPeerConnection.prototype)) { + var _addTrack = window.RTCPeerConnection.prototype.addTrack; + window.RTCPeerConnection.prototype.addStream = function addStream(stream) { + var _this = this; + if (!this._localStreams) { + this._localStreams = []; + } + if (!this._localStreams.includes(stream)) { + this._localStreams.push(stream); + } + // Try to emulate Chrome's behaviour of adding in audio-video order. + // Safari orders by track id. + stream.getAudioTracks().forEach(function (track) { + return _addTrack.call(_this, track, stream); + }); + stream.getVideoTracks().forEach(function (track) { + return _addTrack.call(_this, track, stream); + }); + }; + window.RTCPeerConnection.prototype.addTrack = function addTrack(track) { + var _this2 = this; + for (var _len = arguments.length, streams = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + streams[_key - 1] = arguments[_key]; + } + if (streams) { + streams.forEach(function (stream) { + if (!_this2._localStreams) { + _this2._localStreams = [stream]; + } else if (!_this2._localStreams.includes(stream)) { + _this2._localStreams.push(stream); + } + }); + } + return _addTrack.apply(this, arguments); + }; + } + if (!('removeStream' in window.RTCPeerConnection.prototype)) { + window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { + var _this3 = this; + if (!this._localStreams) { + this._localStreams = []; + } + var index = this._localStreams.indexOf(stream); + if (index === -1) { + return; + } + this._localStreams.splice(index, 1); + var tracks = stream.getTracks(); + this.getSenders().forEach(function (sender) { + if (tracks.includes(sender.track)) { + _this3.removeTrack(sender); + } + }); + }; + } +} +function shimRemoteStreamsAPI(window) { + if (_typeof(window) !== 'object' || !window.RTCPeerConnection) { + return; + } + if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) { + window.RTCPeerConnection.prototype.getRemoteStreams = function getRemoteStreams() { + return this._remoteStreams ? this._remoteStreams : []; + }; + } + if (!('onaddstream' in window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', { + get: function get() { + return this._onaddstream; + }, + set: function set(f) { + var _this4 = this; + if (this._onaddstream) { + this.removeEventListener('addstream', this._onaddstream); + this.removeEventListener('track', this._onaddstreampoly); + } + this.addEventListener('addstream', this._onaddstream = f); + this.addEventListener('track', this._onaddstreampoly = function (e) { + e.streams.forEach(function (stream) { + if (!_this4._remoteStreams) { + _this4._remoteStreams = []; + } + if (_this4._remoteStreams.includes(stream)) { + return; + } + _this4._remoteStreams.push(stream); + var event = new Event('addstream'); + event.stream = stream; + _this4.dispatchEvent(event); + }); + }); + } + }); + var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; + window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() { + var pc = this; + if (!this._onaddstreampoly) { + this.addEventListener('track', this._onaddstreampoly = function (e) { + e.streams.forEach(function (stream) { + if (!pc._remoteStreams) { + pc._remoteStreams = []; + } + if (pc._remoteStreams.indexOf(stream) >= 0) { + return; + } + pc._remoteStreams.push(stream); + var event = new Event('addstream'); + event.stream = stream; + pc.dispatchEvent(event); + }); + }); + } + return origSetRemoteDescription.apply(pc, arguments); + }; + } +} +function shimCallbacksAPI(window) { + if (_typeof(window) !== 'object' || !window.RTCPeerConnection) { + return; + } + var prototype = window.RTCPeerConnection.prototype; + var origCreateOffer = prototype.createOffer; + var origCreateAnswer = prototype.createAnswer; + var setLocalDescription = prototype.setLocalDescription; + var setRemoteDescription = prototype.setRemoteDescription; + var addIceCandidate = prototype.addIceCandidate; + prototype.createOffer = function createOffer(successCallback, failureCallback) { + var options = arguments.length >= 2 ? arguments[2] : arguments[0]; + var promise = origCreateOffer.apply(this, [options]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + prototype.createAnswer = function createAnswer(successCallback, failureCallback) { + var options = arguments.length >= 2 ? arguments[2] : arguments[0]; + var promise = origCreateAnswer.apply(this, [options]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + var withCallback = function withCallback(description, successCallback, failureCallback) { + var promise = setLocalDescription.apply(this, [description]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + prototype.setLocalDescription = withCallback; + withCallback = function withCallback(description, successCallback, failureCallback) { + var promise = setRemoteDescription.apply(this, [description]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + prototype.setRemoteDescription = withCallback; + withCallback = function withCallback(candidate, successCallback, failureCallback) { + var promise = addIceCandidate.apply(this, [candidate]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + prototype.addIceCandidate = withCallback; +} +function shimGetUserMedia(window) { + var navigator = window && window.navigator; + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + // shim not needed in Safari 12.1 + var mediaDevices = navigator.mediaDevices; + var _getUserMedia = mediaDevices.getUserMedia.bind(mediaDevices); + navigator.mediaDevices.getUserMedia = function (constraints) { + return _getUserMedia(shimConstraints(constraints)); + }; + } + if (!navigator.getUserMedia && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + navigator.getUserMedia = function getUserMedia(constraints, cb, errcb) { + navigator.mediaDevices.getUserMedia(constraints).then(cb, errcb); + }.bind(navigator); + } +} +function shimConstraints(constraints) { + if (constraints && constraints.video !== undefined) { + return Object.assign({}, constraints, { + video: utils.compactObject(constraints.video) + }); + } + return constraints; +} +function shimRTCIceServerUrls(window) { + if (!window.RTCPeerConnection) { + return; + } + // migrate from non-spec RTCIceServer.url to RTCIceServer.urls + var OrigPeerConnection = window.RTCPeerConnection; + window.RTCPeerConnection = function RTCPeerConnection(pcConfig, pcConstraints) { + if (pcConfig && pcConfig.iceServers) { + var newIceServers = []; + for (var i = 0; i < pcConfig.iceServers.length; i++) { + var server = pcConfig.iceServers[i]; + if (server.urls === undefined && server.url) { + utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls'); + server = JSON.parse(JSON.stringify(server)); + server.urls = server.url; + delete server.url; + newIceServers.push(server); + } else { + newIceServers.push(pcConfig.iceServers[i]); + } + } + pcConfig.iceServers = newIceServers; + } + return new OrigPeerConnection(pcConfig, pcConstraints); + }; + window.RTCPeerConnection.prototype = OrigPeerConnection.prototype; + // wrap static methods. Currently just generateCertificate. + if ('generateCertificate' in OrigPeerConnection) { + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function get() { + return OrigPeerConnection.generateCertificate; + } + }); + } +} +function shimTrackEventTransceiver(window) { + // Add event.transceiver member over deprecated event.receiver + if (_typeof(window) === 'object' && window.RTCTrackEvent && 'receiver' in window.RTCTrackEvent.prototype && !('transceiver' in window.RTCTrackEvent.prototype)) { + Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { + get: function get() { + return { + receiver: this.receiver + }; + } + }); + } +} +function shimCreateOfferLegacy(window) { + var origCreateOffer = window.RTCPeerConnection.prototype.createOffer; + window.RTCPeerConnection.prototype.createOffer = function createOffer(offerOptions) { + if (offerOptions) { + if (typeof offerOptions.offerToReceiveAudio !== 'undefined') { + // support bit values + offerOptions.offerToReceiveAudio = !!offerOptions.offerToReceiveAudio; + } + var audioTransceiver = this.getTransceivers().find(function (transceiver) { + return transceiver.receiver.track.kind === 'audio'; + }); + if (offerOptions.offerToReceiveAudio === false && audioTransceiver) { + if (audioTransceiver.direction === 'sendrecv') { + if (audioTransceiver.setDirection) { + audioTransceiver.setDirection('sendonly'); + } else { + audioTransceiver.direction = 'sendonly'; + } + } else if (audioTransceiver.direction === 'recvonly') { + if (audioTransceiver.setDirection) { + audioTransceiver.setDirection('inactive'); + } else { + audioTransceiver.direction = 'inactive'; + } + } + } else if (offerOptions.offerToReceiveAudio === true && !audioTransceiver) { + this.addTransceiver('audio', { + direction: 'recvonly' + }); + } + if (typeof offerOptions.offerToReceiveVideo !== 'undefined') { + // support bit values + offerOptions.offerToReceiveVideo = !!offerOptions.offerToReceiveVideo; + } + var videoTransceiver = this.getTransceivers().find(function (transceiver) { + return transceiver.receiver.track.kind === 'video'; + }); + if (offerOptions.offerToReceiveVideo === false && videoTransceiver) { + if (videoTransceiver.direction === 'sendrecv') { + if (videoTransceiver.setDirection) { + videoTransceiver.setDirection('sendonly'); + } else { + videoTransceiver.direction = 'sendonly'; + } + } else if (videoTransceiver.direction === 'recvonly') { + if (videoTransceiver.setDirection) { + videoTransceiver.setDirection('inactive'); + } else { + videoTransceiver.direction = 'inactive'; + } + } + } else if (offerOptions.offerToReceiveVideo === true && !videoTransceiver) { + this.addTransceiver('video', { + direction: 'recvonly' + }); + } + } + return origCreateOffer.apply(this, arguments); + }; +} +function shimAudioContext(window) { + if (_typeof(window) !== 'object' || window.AudioContext) { + return; + } + window.AudioContext = window.webkitAudioContext; +} + +},{"../utils":11}],11:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ +/* eslint-env node */ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.compactObject = compactObject; +exports.deprecated = deprecated; +exports.detectBrowser = detectBrowser; +exports.disableLog = disableLog; +exports.disableWarnings = disableWarnings; +exports.extractVersion = extractVersion; +exports.filterStats = filterStats; +exports.log = log; +exports.walkStats = walkStats; +exports.wrapPeerConnectionEvent = wrapPeerConnectionEvent; +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } +var logDisabled_ = true; +var deprecationWarnings_ = true; + +/** + * Extract browser version out of the provided user agent string. + * + * @param {!string} uastring userAgent string. + * @param {!string} expr Regular expression used as match criteria. + * @param {!number} pos position in the version string to be returned. + * @return {!number} browser version. + */ +function extractVersion(uastring, expr, pos) { + var match = uastring.match(expr); + return match && match.length >= pos && parseInt(match[pos], 10); +} + +// Wraps the peerconnection event eventNameToWrap in a function +// which returns the modified event object (or false to prevent +// the event). +function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) { + if (!window.RTCPeerConnection) { + return; + } + var proto = window.RTCPeerConnection.prototype; + var nativeAddEventListener = proto.addEventListener; + proto.addEventListener = function (nativeEventName, cb) { + if (nativeEventName !== eventNameToWrap) { + return nativeAddEventListener.apply(this, arguments); + } + var wrappedCallback = function wrappedCallback(e) { + var modifiedEvent = wrapper(e); + if (modifiedEvent) { + if (cb.handleEvent) { + cb.handleEvent(modifiedEvent); + } else { + cb(modifiedEvent); + } + } + }; + this._eventMap = this._eventMap || {}; + if (!this._eventMap[eventNameToWrap]) { + this._eventMap[eventNameToWrap] = new Map(); + } + this._eventMap[eventNameToWrap].set(cb, wrappedCallback); + return nativeAddEventListener.apply(this, [nativeEventName, wrappedCallback]); + }; + var nativeRemoveEventListener = proto.removeEventListener; + proto.removeEventListener = function (nativeEventName, cb) { + if (nativeEventName !== eventNameToWrap || !this._eventMap || !this._eventMap[eventNameToWrap]) { + return nativeRemoveEventListener.apply(this, arguments); + } + if (!this._eventMap[eventNameToWrap].has(cb)) { + return nativeRemoveEventListener.apply(this, arguments); + } + var unwrappedCb = this._eventMap[eventNameToWrap].get(cb); + this._eventMap[eventNameToWrap]["delete"](cb); + if (this._eventMap[eventNameToWrap].size === 0) { + delete this._eventMap[eventNameToWrap]; + } + if (Object.keys(this._eventMap).length === 0) { + delete this._eventMap; + } + return nativeRemoveEventListener.apply(this, [nativeEventName, unwrappedCb]); + }; + Object.defineProperty(proto, 'on' + eventNameToWrap, { + get: function get() { + return this['_on' + eventNameToWrap]; + }, + set: function set(cb) { + if (this['_on' + eventNameToWrap]) { + this.removeEventListener(eventNameToWrap, this['_on' + eventNameToWrap]); + delete this['_on' + eventNameToWrap]; + } + if (cb) { + this.addEventListener(eventNameToWrap, this['_on' + eventNameToWrap] = cb); + } + }, + enumerable: true, + configurable: true + }); +} +function disableLog(bool) { + if (typeof bool !== 'boolean') { + return new Error('Argument type: ' + _typeof(bool) + '. Please use a boolean.'); + } + logDisabled_ = bool; + return bool ? 'adapter.js logging disabled' : 'adapter.js logging enabled'; +} + +/** + * Disable or enable deprecation warnings + * @param {!boolean} bool set to true to disable warnings. + */ +function disableWarnings(bool) { + if (typeof bool !== 'boolean') { + return new Error('Argument type: ' + _typeof(bool) + '. Please use a boolean.'); + } + deprecationWarnings_ = !bool; + return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled'); +} +function log() { + if ((typeof window === "undefined" ? "undefined" : _typeof(window)) === 'object') { + if (logDisabled_) { + return; + } + if (typeof console !== 'undefined' && typeof console.log === 'function') { + console.log.apply(console, arguments); + } + } +} + +/** + * Shows a deprecation warning suggesting the modern and spec-compatible API. + */ +function deprecated(oldMethod, newMethod) { + if (!deprecationWarnings_) { + return; + } + console.warn(oldMethod + ' is deprecated, please use ' + newMethod + ' instead.'); +} + +/** + * Browser detector. + * + * @return {object} result containing browser and version + * properties. + */ +function detectBrowser(window) { + // Returned result object. + var result = { + browser: null, + version: null + }; + + // Fail early if it's not a browser + if (typeof window === 'undefined' || !window.navigator) { + result.browser = 'Not a browser.'; + return result; + } + var navigator = window.navigator; + if (navigator.mozGetUserMedia) { + // Firefox. + result.browser = 'firefox'; + result.version = extractVersion(navigator.userAgent, /Firefox\/(\d+)\./, 1); + } else if (navigator.webkitGetUserMedia || window.isSecureContext === false && window.webkitRTCPeerConnection) { + // Chrome, Chromium, Webview, Opera. + // Version matches Chrome/WebRTC version. + // Chrome 74 removed webkitGetUserMedia on http as well so we need the + // more complicated fallback to webkitRTCPeerConnection. + result.browser = 'chrome'; + result.version = extractVersion(navigator.userAgent, /Chrom(e|ium)\/(\d+)\./, 2); + } else if (window.RTCPeerConnection && navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { + // Safari. + result.browser = 'safari'; + result.version = extractVersion(navigator.userAgent, /AppleWebKit\/(\d+)\./, 1); + result.supportsUnifiedPlan = window.RTCRtpTransceiver && 'currentDirection' in window.RTCRtpTransceiver.prototype; + } else { + // Default fallthrough: not supported. + result.browser = 'Not a supported browser.'; + return result; + } + return result; +} + +/** + * Checks if something is an object. + * + * @param {*} val The something you want to check. + * @return true if val is an object, false otherwise. + */ +function isObject(val) { + return Object.prototype.toString.call(val) === '[object Object]'; +} + +/** + * Remove all empty objects and undefined values + * from a nested object -- an enhanced and vanilla version + * of Lodash's `compact`. + */ +function compactObject(data) { + if (!isObject(data)) { + return data; + } + return Object.keys(data).reduce(function (accumulator, key) { + var isObj = isObject(data[key]); + var value = isObj ? compactObject(data[key]) : data[key]; + var isEmptyObject = isObj && !Object.keys(value).length; + if (value === undefined || isEmptyObject) { + return accumulator; + } + return Object.assign(accumulator, _defineProperty({}, key, value)); + }, {}); +} + +/* iterates the stats graph recursively. */ +function walkStats(stats, base, resultSet) { + if (!base || resultSet.has(base.id)) { + return; + } + resultSet.set(base.id, base); + Object.keys(base).forEach(function (name) { + if (name.endsWith('Id')) { + walkStats(stats, stats.get(base[name]), resultSet); + } else if (name.endsWith('Ids')) { + base[name].forEach(function (id) { + walkStats(stats, stats.get(id), resultSet); + }); + } + }); +} + +/* filter getStats for a sender/receiver track. */ +function filterStats(result, track, outbound) { + var streamStatsType = outbound ? 'outbound-rtp' : 'inbound-rtp'; + var filteredResult = new Map(); + if (track === null) { + return filteredResult; + } + var trackStats = []; + result.forEach(function (value) { + if (value.type === 'track' && value.trackIdentifier === track.id) { + trackStats.push(value); + } + }); + trackStats.forEach(function (trackStat) { + result.forEach(function (stats) { + if (stats.type === streamStatsType && stats.trackId === trackStat.id) { + walkStats(result, stats, filteredResult); + } + }); + }); + return filteredResult; +} + +},{}],12:[function(require,module,exports){ +/* eslint-env node */ +'use strict'; + +// SDP helpers. + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var SDPUtils = {}; + +// Generate an alphanumeric identifier for cname or mids. +// TODO: use UUIDs instead? https://gist.github.com/jed/982883 +SDPUtils.generateIdentifier = function () { + return Math.random().toString(36).substring(2, 12); +}; + +// The RTCP CNAME used by all peerconnections from the same JS. +SDPUtils.localCName = SDPUtils.generateIdentifier(); + +// Splits SDP into lines, dealing with both CRLF and LF. +SDPUtils.splitLines = function (blob) { + return blob.trim().split('\n').map(function (line) { + return line.trim(); + }); +}; +// Splits SDP into sessionpart and mediasections. Ensures CRLF. +SDPUtils.splitSections = function (blob) { + var parts = blob.split('\nm='); + return parts.map(function (part, index) { + return (index > 0 ? 'm=' + part : part).trim() + '\r\n'; + }); +}; + +// Returns the session description. +SDPUtils.getDescription = function (blob) { + var sections = SDPUtils.splitSections(blob); + return sections && sections[0]; +}; + +// Returns the individual media sections. +SDPUtils.getMediaSections = function (blob) { + var sections = SDPUtils.splitSections(blob); + sections.shift(); + return sections; +}; + +// Returns lines that start with a certain prefix. +SDPUtils.matchPrefix = function (blob, prefix) { + return SDPUtils.splitLines(blob).filter(function (line) { + return line.indexOf(prefix) === 0; + }); +}; + +// Parses an ICE candidate line. Sample input: +// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 +// rport 55996" +// Input can be prefixed with a=. +SDPUtils.parseCandidate = function (line) { + var parts = void 0; + // Parse both variants. + if (line.indexOf('a=candidate:') === 0) { + parts = line.substring(12).split(' '); + } else { + parts = line.substring(10).split(' '); + } + + var candidate = { + foundation: parts[0], + component: { 1: 'rtp', 2: 'rtcp' }[parts[1]] || parts[1], + protocol: parts[2].toLowerCase(), + priority: parseInt(parts[3], 10), + ip: parts[4], + address: parts[4], // address is an alias for ip. + port: parseInt(parts[5], 10), + // skip parts[6] == 'typ' + type: parts[7] + }; + + for (var i = 8; i < parts.length; i += 2) { + switch (parts[i]) { + case 'raddr': + candidate.relatedAddress = parts[i + 1]; + break; + case 'rport': + candidate.relatedPort = parseInt(parts[i + 1], 10); + break; + case 'tcptype': + candidate.tcpType = parts[i + 1]; + break; + case 'ufrag': + candidate.ufrag = parts[i + 1]; // for backward compatibility. + candidate.usernameFragment = parts[i + 1]; + break; + default: + // extension handling, in particular ufrag. Don't overwrite. + if (candidate[parts[i]] === undefined) { + candidate[parts[i]] = parts[i + 1]; + } + break; + } + } + return candidate; +}; + +// Translates a candidate object into SDP candidate attribute. +// This does not include the a= prefix! +SDPUtils.writeCandidate = function (candidate) { + var sdp = []; + sdp.push(candidate.foundation); + + var component = candidate.component; + if (component === 'rtp') { + sdp.push(1); + } else if (component === 'rtcp') { + sdp.push(2); + } else { + sdp.push(component); + } + sdp.push(candidate.protocol.toUpperCase()); + sdp.push(candidate.priority); + sdp.push(candidate.address || candidate.ip); + sdp.push(candidate.port); + + var type = candidate.type; + sdp.push('typ'); + sdp.push(type); + if (type !== 'host' && candidate.relatedAddress && candidate.relatedPort) { + sdp.push('raddr'); + sdp.push(candidate.relatedAddress); + sdp.push('rport'); + sdp.push(candidate.relatedPort); + } + if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { + sdp.push('tcptype'); + sdp.push(candidate.tcpType); + } + if (candidate.usernameFragment || candidate.ufrag) { + sdp.push('ufrag'); + sdp.push(candidate.usernameFragment || candidate.ufrag); + } + return 'candidate:' + sdp.join(' '); +}; + +// Parses an ice-options line, returns an array of option tags. +// Sample input: +// a=ice-options:foo bar +SDPUtils.parseIceOptions = function (line) { + return line.substring(14).split(' '); +}; + +// Parses a rtpmap line, returns RTCRtpCoddecParameters. Sample input: +// a=rtpmap:111 opus/48000/2 +SDPUtils.parseRtpMap = function (line) { + var parts = line.substring(9).split(' '); + var parsed = { + payloadType: parseInt(parts.shift(), 10) // was: id + }; + + parts = parts[0].split('/'); + + parsed.name = parts[0]; + parsed.clockRate = parseInt(parts[1], 10); // was: clockrate + parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1; + // legacy alias, got renamed back to channels in ORTC. + parsed.numChannels = parsed.channels; + return parsed; +}; + +// Generates a rtpmap line from RTCRtpCodecCapability or +// RTCRtpCodecParameters. +SDPUtils.writeRtpMap = function (codec) { + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + var channels = codec.channels || codec.numChannels || 1; + return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + (channels !== 1 ? '/' + channels : '') + '\r\n'; +}; + +// Parses a extmap line (headerextension from RFC 5285). Sample input: +// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset +// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset +SDPUtils.parseExtmap = function (line) { + var parts = line.substring(9).split(' '); + return { + id: parseInt(parts[0], 10), + direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv', + uri: parts[1], + attributes: parts.slice(2).join(' ') + }; +}; + +// Generates an extmap line from RTCRtpHeaderExtensionParameters or +// RTCRtpHeaderExtension. +SDPUtils.writeExtmap = function (headerExtension) { + return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + (headerExtension.direction && headerExtension.direction !== 'sendrecv' ? '/' + headerExtension.direction : '') + ' ' + headerExtension.uri + (headerExtension.attributes ? ' ' + headerExtension.attributes : '') + '\r\n'; +}; + +// Parses a fmtp line, returns dictionary. Sample input: +// a=fmtp:96 vbr=on;cng=on +// Also deals with vbr=on; cng=on +SDPUtils.parseFmtp = function (line) { + var parsed = {}; + var kv = void 0; + var parts = line.substring(line.indexOf(' ') + 1).split(';'); + for (var j = 0; j < parts.length; j++) { + kv = parts[j].trim().split('='); + parsed[kv[0].trim()] = kv[1]; + } + return parsed; +}; + +// Generates a fmtp line from RTCRtpCodecCapability or RTCRtpCodecParameters. +SDPUtils.writeFmtp = function (codec) { + var line = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.parameters && Object.keys(codec.parameters).length) { + var params = []; + Object.keys(codec.parameters).forEach(function (param) { + if (codec.parameters[param] !== undefined) { + params.push(param + '=' + codec.parameters[param]); + } else { + params.push(param); + } + }); + line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; + } + return line; +}; + +// Parses a rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: +// a=rtcp-fb:98 nack rpsi +SDPUtils.parseRtcpFb = function (line) { + var parts = line.substring(line.indexOf(' ') + 1).split(' '); + return { + type: parts.shift(), + parameter: parts.join(' ') + }; +}; + +// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. +SDPUtils.writeRtcpFb = function (codec) { + var lines = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.rtcpFeedback && codec.rtcpFeedback.length) { + // FIXME: special handling for trr-int? + codec.rtcpFeedback.forEach(function (fb) { + lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') + '\r\n'; + }); + } + return lines; +}; + +// Parses a RFC 5576 ssrc media attribute. Sample input: +// a=ssrc:3735928559 cname:something +SDPUtils.parseSsrcMedia = function (line) { + var sp = line.indexOf(' '); + var parts = { + ssrc: parseInt(line.substring(7, sp), 10) + }; + var colon = line.indexOf(':', sp); + if (colon > -1) { + parts.attribute = line.substring(sp + 1, colon); + parts.value = line.substring(colon + 1); + } else { + parts.attribute = line.substring(sp + 1); + } + return parts; +}; + +// Parse a ssrc-group line (see RFC 5576). Sample input: +// a=ssrc-group:semantics 12 34 +SDPUtils.parseSsrcGroup = function (line) { + var parts = line.substring(13).split(' '); + return { + semantics: parts.shift(), + ssrcs: parts.map(function (ssrc) { + return parseInt(ssrc, 10); + }) + }; +}; + +// Extracts the MID (RFC 5888) from a media section. +// Returns the MID or undefined if no mid line was found. +SDPUtils.getMid = function (mediaSection) { + var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0]; + if (mid) { + return mid.substring(6); + } +}; + +// Parses a fingerprint line for DTLS-SRTP. +SDPUtils.parseFingerprint = function (line) { + var parts = line.substring(14).split(' '); + return { + algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge. + value: parts[1].toUpperCase() // the definition is upper-case in RFC 4572. + }; +}; + +// Extracts DTLS parameters from SDP media section or sessionpart. +// FIXME: for consistency with other functions this should only +// get the fingerprint line as input. See also getIceParameters. +SDPUtils.getDtlsParameters = function (mediaSection, sessionpart) { + var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=fingerprint:'); + // Note: a=setup line is ignored since we use the 'auto' role in Edge. + return { + role: 'auto', + fingerprints: lines.map(SDPUtils.parseFingerprint) + }; +}; + +// Serializes DTLS parameters to SDP. +SDPUtils.writeDtlsParameters = function (params, setupType) { + var sdp = 'a=setup:' + setupType + '\r\n'; + params.fingerprints.forEach(function (fp) { + sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; + }); + return sdp; +}; + +// Parses a=crypto lines into +// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members +SDPUtils.parseCryptoLine = function (line) { + var parts = line.substring(9).split(' '); + return { + tag: parseInt(parts[0], 10), + cryptoSuite: parts[1], + keyParams: parts[2], + sessionParams: parts.slice(3) + }; +}; + +SDPUtils.writeCryptoLine = function (parameters) { + return 'a=crypto:' + parameters.tag + ' ' + parameters.cryptoSuite + ' ' + (_typeof(parameters.keyParams) === 'object' ? SDPUtils.writeCryptoKeyParams(parameters.keyParams) : parameters.keyParams) + (parameters.sessionParams ? ' ' + parameters.sessionParams.join(' ') : '') + '\r\n'; +}; + +// Parses the crypto key parameters into +// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam* +SDPUtils.parseCryptoKeyParams = function (keyParams) { + if (keyParams.indexOf('inline:') !== 0) { + return null; + } + var parts = keyParams.substring(7).split('|'); + return { + keyMethod: 'inline', + keySalt: parts[0], + lifeTime: parts[1], + mkiValue: parts[2] ? parts[2].split(':')[0] : undefined, + mkiLength: parts[2] ? parts[2].split(':')[1] : undefined + }; +}; + +SDPUtils.writeCryptoKeyParams = function (keyParams) { + return keyParams.keyMethod + ':' + keyParams.keySalt + (keyParams.lifeTime ? '|' + keyParams.lifeTime : '') + (keyParams.mkiValue && keyParams.mkiLength ? '|' + keyParams.mkiValue + ':' + keyParams.mkiLength : ''); +}; + +// Extracts all SDES parameters. +SDPUtils.getCryptoParameters = function (mediaSection, sessionpart) { + var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=crypto:'); + return lines.map(SDPUtils.parseCryptoLine); +}; + +// Parses ICE information from SDP media section or sessionpart. +// FIXME: for consistency with other functions this should only +// get the ice-ufrag and ice-pwd lines as input. +SDPUtils.getIceParameters = function (mediaSection, sessionpart) { + var ufrag = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=ice-ufrag:')[0]; + var pwd = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=ice-pwd:')[0]; + if (!(ufrag && pwd)) { + return null; + } + return { + usernameFragment: ufrag.substring(12), + password: pwd.substring(10) + }; +}; + +// Serializes ICE parameters to SDP. +SDPUtils.writeIceParameters = function (params) { + var sdp = 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + 'a=ice-pwd:' + params.password + '\r\n'; + if (params.iceLite) { + sdp += 'a=ice-lite\r\n'; + } + return sdp; +}; + +// Parses the SDP media section and returns RTCRtpParameters. +SDPUtils.parseRtpParameters = function (mediaSection) { + var description = { + codecs: [], + headerExtensions: [], + fecMechanisms: [], + rtcp: [] + }; + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(' '); + description.profile = mline[2]; + for (var i = 3; i < mline.length; i++) { + // find all codecs from mline[3..] + var pt = mline[i]; + var rtpmapline = SDPUtils.matchPrefix(mediaSection, 'a=rtpmap:' + pt + ' ')[0]; + if (rtpmapline) { + var codec = SDPUtils.parseRtpMap(rtpmapline); + var fmtps = SDPUtils.matchPrefix(mediaSection, 'a=fmtp:' + pt + ' '); + // Only the first a=fmtp: is considered. + codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; + codec.rtcpFeedback = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-fb:' + pt + ' ').map(SDPUtils.parseRtcpFb); + description.codecs.push(codec); + // parse FEC mechanisms from rtpmap lines. + switch (codec.name.toUpperCase()) { + case 'RED': + case 'ULPFEC': + description.fecMechanisms.push(codec.name.toUpperCase()); + break; + default: + // only RED and ULPFEC are recognized as FEC mechanisms. + break; + } + } + } + SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function (line) { + description.headerExtensions.push(SDPUtils.parseExtmap(line)); + }); + var wildcardRtcpFb = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-fb:* ').map(SDPUtils.parseRtcpFb); + description.codecs.forEach(function (codec) { + wildcardRtcpFb.forEach(function (fb) { + var duplicate = codec.rtcpFeedback.find(function (existingFeedback) { + return existingFeedback.type === fb.type && existingFeedback.parameter === fb.parameter; + }); + if (!duplicate) { + codec.rtcpFeedback.push(fb); + } + }); + }); + // FIXME: parse rtcp. + return description; +}; + +// Generates parts of the SDP media section describing the capabilities / +// parameters. +SDPUtils.writeRtpDescription = function (kind, caps) { + var sdp = ''; + + // Build the mline. + sdp += 'm=' + kind + ' '; + sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. + sdp += ' ' + (caps.profile || 'UDP/TLS/RTP/SAVPF') + ' '; + sdp += caps.codecs.map(function (codec) { + if (codec.preferredPayloadType !== undefined) { + return codec.preferredPayloadType; + } + return codec.payloadType; + }).join(' ') + '\r\n'; + + sdp += 'c=IN IP4 0.0.0.0\r\n'; + sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; + + // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. + caps.codecs.forEach(function (codec) { + sdp += SDPUtils.writeRtpMap(codec); + sdp += SDPUtils.writeFmtp(codec); + sdp += SDPUtils.writeRtcpFb(codec); + }); + var maxptime = 0; + caps.codecs.forEach(function (codec) { + if (codec.maxptime > maxptime) { + maxptime = codec.maxptime; + } + }); + if (maxptime > 0) { + sdp += 'a=maxptime:' + maxptime + '\r\n'; + } + + if (caps.headerExtensions) { + caps.headerExtensions.forEach(function (extension) { + sdp += SDPUtils.writeExtmap(extension); + }); + } + // FIXME: write fecMechanisms. + return sdp; +}; + +// Parses the SDP media section and returns an array of +// RTCRtpEncodingParameters. +SDPUtils.parseRtpEncodingParameters = function (mediaSection) { + var encodingParameters = []; + var description = SDPUtils.parseRtpParameters(mediaSection); + var hasRed = description.fecMechanisms.indexOf('RED') !== -1; + var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1; + + // filter a=ssrc:... cname:, ignore PlanB-msid + var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:').map(function (line) { + return SDPUtils.parseSsrcMedia(line); + }).filter(function (parts) { + return parts.attribute === 'cname'; + }); + var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; + var secondarySsrc = void 0; + + var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID').map(function (line) { + var parts = line.substring(17).split(' '); + return parts.map(function (part) { + return parseInt(part, 10); + }); + }); + if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { + secondarySsrc = flows[0][1]; + } + + description.codecs.forEach(function (codec) { + if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { + var encParam = { + ssrc: primarySsrc, + codecPayloadType: parseInt(codec.parameters.apt, 10) + }; + if (primarySsrc && secondarySsrc) { + encParam.rtx = { ssrc: secondarySsrc }; + } + encodingParameters.push(encParam); + if (hasRed) { + encParam = JSON.parse(JSON.stringify(encParam)); + encParam.fec = { + ssrc: primarySsrc, + mechanism: hasUlpfec ? 'red+ulpfec' : 'red' + }; + encodingParameters.push(encParam); + } + } + }); + if (encodingParameters.length === 0 && primarySsrc) { + encodingParameters.push({ + ssrc: primarySsrc + }); + } + + // we support both b=AS and b=TIAS but interpret AS as TIAS. + var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b='); + if (bandwidth.length) { + if (bandwidth[0].indexOf('b=TIAS:') === 0) { + bandwidth = parseInt(bandwidth[0].substring(7), 10); + } else if (bandwidth[0].indexOf('b=AS:') === 0) { + // use formula from JSEP to convert b=AS to TIAS value. + bandwidth = parseInt(bandwidth[0].substring(5), 10) * 1000 * 0.95 - 50 * 40 * 8; + } else { + bandwidth = undefined; + } + encodingParameters.forEach(function (params) { + params.maxBitrate = bandwidth; + }); + } + return encodingParameters; +}; + +// parses http://draft.ortc.org/#rtcrtcpparameters* +SDPUtils.parseRtcpParameters = function (mediaSection) { + var rtcpParameters = {}; + + // Gets the first SSRC. Note that with RTX there might be multiple + // SSRCs. + var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:').map(function (line) { + return SDPUtils.parseSsrcMedia(line); + }).filter(function (obj) { + return obj.attribute === 'cname'; + })[0]; + if (remoteSsrc) { + rtcpParameters.cname = remoteSsrc.value; + rtcpParameters.ssrc = remoteSsrc.ssrc; + } + + // Edge uses the compound attribute instead of reducedSize + // compound is !reducedSize + var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize'); + rtcpParameters.reducedSize = rsize.length > 0; + rtcpParameters.compound = rsize.length === 0; + + // parses the rtcp-mux attrіbute. + // Note that Edge does not support unmuxed RTCP. + var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux'); + rtcpParameters.mux = mux.length > 0; + + return rtcpParameters; +}; + +SDPUtils.writeRtcpParameters = function (rtcpParameters) { + var sdp = ''; + if (rtcpParameters.reducedSize) { + sdp += 'a=rtcp-rsize\r\n'; + } + if (rtcpParameters.mux) { + sdp += 'a=rtcp-mux\r\n'; + } + if (rtcpParameters.ssrc !== undefined && rtcpParameters.cname) { + sdp += 'a=ssrc:' + rtcpParameters.ssrc + ' cname:' + rtcpParameters.cname + '\r\n'; + } + return sdp; +}; + +// parses either a=msid: or a=ssrc:... msid lines and returns +// the id of the MediaStream and MediaStreamTrack. +SDPUtils.parseMsid = function (mediaSection) { + var parts = void 0; + var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:'); + if (spec.length === 1) { + parts = spec[0].substring(7).split(' '); + return { stream: parts[0], track: parts[1] }; + } + var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:').map(function (line) { + return SDPUtils.parseSsrcMedia(line); + }).filter(function (msidParts) { + return msidParts.attribute === 'msid'; + }); + if (planB.length > 0) { + parts = planB[0].value.split(' '); + return { stream: parts[0], track: parts[1] }; + } +}; + +// SCTP +// parses draft-ietf-mmusic-sctp-sdp-26 first and falls back +// to draft-ietf-mmusic-sctp-sdp-05 +SDPUtils.parseSctpDescription = function (mediaSection) { + var mline = SDPUtils.parseMLine(mediaSection); + var maxSizeLine = SDPUtils.matchPrefix(mediaSection, 'a=max-message-size:'); + var maxMessageSize = void 0; + if (maxSizeLine.length > 0) { + maxMessageSize = parseInt(maxSizeLine[0].substring(19), 10); + } + if (isNaN(maxMessageSize)) { + maxMessageSize = 65536; + } + var sctpPort = SDPUtils.matchPrefix(mediaSection, 'a=sctp-port:'); + if (sctpPort.length > 0) { + return { + port: parseInt(sctpPort[0].substring(12), 10), + protocol: mline.fmt, + maxMessageSize: maxMessageSize + }; + } + var sctpMapLines = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:'); + if (sctpMapLines.length > 0) { + var parts = sctpMapLines[0].substring(10).split(' '); + return { + port: parseInt(parts[0], 10), + protocol: parts[1], + maxMessageSize: maxMessageSize + }; + } +}; + +// SCTP +// outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers +// support by now receiving in this format, unless we originally parsed +// as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line +// protocol of DTLS/SCTP -- without UDP/ or TCP/) +SDPUtils.writeSctpDescription = function (media, sctp) { + var output = []; + if (media.protocol !== 'DTLS/SCTP') { + output = ['m=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.protocol + '\r\n', 'c=IN IP4 0.0.0.0\r\n', 'a=sctp-port:' + sctp.port + '\r\n']; + } else { + output = ['m=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.port + '\r\n', 'c=IN IP4 0.0.0.0\r\n', 'a=sctpmap:' + sctp.port + ' ' + sctp.protocol + ' 65535\r\n']; + } + if (sctp.maxMessageSize !== undefined) { + output.push('a=max-message-size:' + sctp.maxMessageSize + '\r\n'); + } + return output.join(''); +}; + +// Generate a session ID for SDP. +// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1 +// recommends using a cryptographically random +ve 64-bit value +// but right now this should be acceptable and within the right range +SDPUtils.generateSessionId = function () { + return Math.random().toString().substr(2, 22); +}; + +// Write boiler plate for start of SDP +// sessId argument is optional - if not supplied it will +// be generated randomly +// sessVersion is optional and defaults to 2 +// sessUser is optional and defaults to 'thisisadapterortc' +SDPUtils.writeSessionBoilerplate = function (sessId, sessVer, sessUser) { + var sessionId = void 0; + var version = sessVer !== undefined ? sessVer : 2; + if (sessId) { + sessionId = sessId; + } else { + sessionId = SDPUtils.generateSessionId(); + } + var user = sessUser || 'thisisadapterortc'; + // FIXME: sess-id should be an NTP timestamp. + return 'v=0\r\n' + 'o=' + user + ' ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' + 's=-\r\n' + 't=0 0\r\n'; +}; + +// Gets the direction from the mediaSection or the sessionpart. +SDPUtils.getDirection = function (mediaSection, sessionpart) { + // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. + var lines = SDPUtils.splitLines(mediaSection); + for (var i = 0; i < lines.length; i++) { + switch (lines[i]) { + case 'a=sendrecv': + case 'a=sendonly': + case 'a=recvonly': + case 'a=inactive': + return lines[i].substring(2); + default: + // FIXME: What should happen here? + } + } + if (sessionpart) { + return SDPUtils.getDirection(sessionpart); + } + return 'sendrecv'; +}; + +SDPUtils.getKind = function (mediaSection) { + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(' '); + return mline[0].substring(2); +}; + +SDPUtils.isRejected = function (mediaSection) { + return mediaSection.split(' ', 2)[1] === '0'; +}; + +SDPUtils.parseMLine = function (mediaSection) { + var lines = SDPUtils.splitLines(mediaSection); + var parts = lines[0].substring(2).split(' '); + return { + kind: parts[0], + port: parseInt(parts[1], 10), + protocol: parts[2], + fmt: parts.slice(3).join(' ') + }; +}; + +SDPUtils.parseOLine = function (mediaSection) { + var line = SDPUtils.matchPrefix(mediaSection, 'o=')[0]; + var parts = line.substring(2).split(' '); + return { + username: parts[0], + sessionId: parts[1], + sessionVersion: parseInt(parts[2], 10), + netType: parts[3], + addressType: parts[4], + address: parts[5] + }; +}; + +// a very naive interpretation of a valid SDP. +SDPUtils.isValidSDP = function (blob) { + if (typeof blob !== 'string' || blob.length === 0) { + return false; + } + var lines = SDPUtils.splitLines(blob); + for (var i = 0; i < lines.length; i++) { + if (lines[i].length < 2 || lines[i].charAt(1) !== '=') { + return false; + } + // TODO: check the modifier a bit more. + } + return true; +}; + +// Expose public methods. +if ((typeof module === 'undefined' ? 'undefined' : _typeof(module)) === 'object') { + module.exports = SDPUtils; +} +},{}]},{},[1])(1) +}); diff --git a/web/share/js/kvm/atx.js b/kvmd_data/usr/share/kvmd/web/share/js/kvm/atx.js similarity index 100% rename from web/share/js/kvm/atx.js rename to kvmd_data/usr/share/kvmd/web/share/js/kvm/atx.js diff --git a/web/share/js/kvm/gpio.js b/kvmd_data/usr/share/kvmd/web/share/js/kvm/gpio.js similarity index 100% rename from web/share/js/kvm/gpio.js rename to kvmd_data/usr/share/kvmd/web/share/js/kvm/gpio.js diff --git a/web/share/js/kvm/hid.js b/kvmd_data/usr/share/kvmd/web/share/js/kvm/hid.js similarity index 100% rename from web/share/js/kvm/hid.js rename to kvmd_data/usr/share/kvmd/web/share/js/kvm/hid.js diff --git a/kvmd_data/usr/share/kvmd/web/share/js/kvm/janus.js b/kvmd_data/usr/share/kvmd/web/share/js/kvm/janus.js new file mode 100644 index 00000000..4544ccfc --- /dev/null +++ b/kvmd_data/usr/share/kvmd/web/share/js/kvm/janus.js @@ -0,0 +1,3651 @@ +import "./adapter.js" +"use strict"; + +/* + The MIT License (MIT) + + Copyright (c) 2016 Meetecho + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + */ + +// List of sessions +Janus.sessions = {}; + +Janus.isExtensionEnabled = function() { + if(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) { + // No need for the extension, getDisplayMedia is supported + return true; + } + if(window.navigator.userAgent.match('Chrome')) { + var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10); + var maxver = 33; + if(window.navigator.userAgent.match('Linux')) + maxver = 35; // "known" crash in chrome 34 and 35 on linux + if(chromever >= 26 && chromever <= maxver) { + // Older versions of Chrome don't support this extension-based approach, so lie + return true; + } + return Janus.extension.isInstalled(); + } else { + // Firefox and others, no need for the extension (but this doesn't mean it will work) + return true; + } +}; + +var defaultExtension = { + // Screensharing Chrome Extension ID + extensionId: 'hapfgfdkleiggjjpfpenajgdnfckjpaj', + isInstalled: function() { return document.querySelector('#janus-extension-installed') !== null; }, + getScreen: function (callback) { + var pending = window.setTimeout(function () { + var error = new Error('NavigatorUserMediaError'); + error.name = 'The required Chrome extension is not installed: click here to install it. (NOTE: this will need you to refresh the page)'; + return callback(error); + }, 1000); + this.cache[pending] = callback; + window.postMessage({ type: 'janusGetScreen', id: pending }, '*'); + }, + init: function () { + var cache = {}; + this.cache = cache; + // Wait for events from the Chrome Extension + window.addEventListener('message', function (event) { + if(event.origin != window.location.origin) + return; + if(event.data.type == 'janusGotScreen' && cache[event.data.id]) { + var callback = cache[event.data.id]; + delete cache[event.data.id]; + + if (event.data.sourceId === '') { + // user canceled + var error = new Error('NavigatorUserMediaError'); + error.name = 'You cancelled the request for permission, giving up...'; + callback(error); + } else { + callback(null, event.data.sourceId); + } + } else if (event.data.type == 'janusGetScreenPending') { + console.log('clearing ', event.data.id); + window.clearTimeout(event.data.id); + } + }); + } +}; + +Janus.useDefaultDependencies = function (deps) { + var f = (deps && deps.fetch) || fetch; + var p = (deps && deps.Promise) || Promise; + var socketCls = (deps && deps.WebSocket) || WebSocket; + + return { + newWebSocket: function(server, proto) { return new socketCls(server, proto); }, + extension: (deps && deps.extension) || defaultExtension, + isArray: function(arr) { return Array.isArray(arr); }, + webRTCAdapter: (deps && deps.adapter) || adapter, + httpAPICall: function(url, options) { + var fetchOptions = { + method: options.verb, + headers: { + 'Accept': 'application/json, text/plain, */*' + }, + cache: 'no-cache' + }; + if(options.verb === "POST") { + fetchOptions.headers['Content-Type'] = 'application/json'; + } + if(options.withCredentials !== undefined) { + fetchOptions.credentials = options.withCredentials === true ? 'include' : (options.withCredentials ? options.withCredentials : 'omit'); + } + if(options.body) { + fetchOptions.body = JSON.stringify(options.body); + } + + var fetching = f(url, fetchOptions).catch(function(error) { + return p.reject({message: 'Probably a network error, is the server down?', error: error}); + }); + + /* + * fetch() does not natively support timeouts. + * Work around this by starting a timeout manually, and racing it agains the fetch() to see which thing resolves first. + */ + + if(options.timeout) { + var timeout = new p(function(resolve, reject) { + var timerId = setTimeout(function() { + clearTimeout(timerId); + return reject({message: 'Request timed out', timeout: options.timeout}); + }, options.timeout); + }); + fetching = p.race([fetching, timeout]); + } + + fetching.then(function(response) { + if(response.ok) { + if(typeof(options.success) === typeof(Janus.noop)) { + return response.json().then(function(parsed) { + try { + options.success(parsed); + } catch(error) { + Janus.error('Unhandled httpAPICall success callback error', error); + } + }, function(error) { + return p.reject({message: 'Failed to parse response body', error: error, response: response}); + }); + } + } + else { + return p.reject({message: 'API call failed', response: response}); + } + }).catch(function(error) { + if(typeof(options.error) === typeof(Janus.noop)) { + options.error(error.message || '<< internal error >>', error); + } + }); + + return fetching; + } + } +}; + +Janus.useOldDependencies = function (deps) { + var jq = (deps && deps.jQuery) || jQuery; + var socketCls = (deps && deps.WebSocket) || WebSocket; + return { + newWebSocket: function(server, proto) { return new socketCls(server, proto); }, + isArray: function(arr) { return jq.isArray(arr); }, + extension: (deps && deps.extension) || defaultExtension, + webRTCAdapter: (deps && deps.adapter) || adapter, + httpAPICall: function(url, options) { + var payload = options.body !== undefined ? { + contentType: 'application/json', + data: JSON.stringify(options.body) + } : {}; + var credentials = options.withCredentials !== undefined ? {xhrFields: {withCredentials: options.withCredentials}} : {}; + + return jq.ajax(jq.extend(payload, credentials, { + url: url, + type: options.verb, + cache: false, + dataType: 'json', + async: options.async, + timeout: options.timeout, + success: function(result) { + if(typeof(options.success) === typeof(Janus.noop)) { + options.success(result); + } + }, + error: function(xhr, status, err) { + if(typeof(options.error) === typeof(Janus.noop)) { + options.error(status, err); + } + } + })); + } + }; +}; + +Janus.noop = function() {}; + +Janus.dataChanDefaultLabel = "JanusDataChannel"; + +// Note: in the future we may want to change this, e.g., as was +// attempted in https://github.com/meetecho/janus-gateway/issues/1670 +Janus.endOfCandidates = null; + +// Stop all tracks from a given stream +Janus.stopAllTracks = function(stream) { + try { + // Try a MediaStreamTrack.stop() for each track + var tracks = stream.getTracks(); + for(var mst of tracks) { + Janus.log(mst); + if(mst) { + mst.stop(); + } + } + } catch(e) { + // Do nothing if this fails + } +} + +// Initialization +Janus.init = function(options) { + options = options || {}; + options.callback = (typeof options.callback == "function") ? options.callback : Janus.noop; + if(Janus.initDone) { + // Already initialized + options.callback(); + } else { + if(typeof console.log == "undefined") { + console.log = function() {}; + } + // Console logging (all debugging disabled by default) + Janus.trace = Janus.noop; + Janus.debug = Janus.noop; + Janus.vdebug = Janus.noop; + Janus.log = Janus.noop; + Janus.warn = Janus.noop; + Janus.error = Janus.noop; + if(options.debug === true || options.debug === "all") { + // Enable all debugging levels + Janus.trace = console.trace.bind(console); + Janus.debug = console.debug.bind(console); + Janus.vdebug = console.debug.bind(console); + Janus.log = console.log.bind(console); + Janus.warn = console.warn.bind(console); + Janus.error = console.error.bind(console); + } else if(Array.isArray(options.debug)) { + for(var d of options.debug) { + switch(d) { + case "trace": + Janus.trace = console.trace.bind(console); + break; + case "debug": + Janus.debug = console.debug.bind(console); + break; + case "vdebug": + Janus.vdebug = console.debug.bind(console); + break; + case "log": + Janus.log = console.log.bind(console); + break; + case "warn": + Janus.warn = console.warn.bind(console); + break; + case "error": + Janus.error = console.error.bind(console); + break; + default: + console.error("Unknown debugging option '" + d + "' (supported: 'trace', 'debug', 'vdebug', 'log', warn', 'error')"); + break; + } + } + } + Janus.log("Initializing library"); + + var usedDependencies = options.dependencies || Janus.useDefaultDependencies(); + Janus.isArray = usedDependencies.isArray; + Janus.webRTCAdapter = usedDependencies.webRTCAdapter; + Janus.httpAPICall = usedDependencies.httpAPICall; + Janus.newWebSocket = usedDependencies.newWebSocket; + Janus.extension = usedDependencies.extension; + Janus.extension.init(); + + // Helper method to enumerate devices + Janus.listDevices = function(callback, config) { + callback = (typeof callback == "function") ? callback : Janus.noop; + if (config == null) config = { audio: true, video: true }; + if(Janus.isGetUserMediaAvailable()) { + navigator.mediaDevices.getUserMedia(config) + .then(function(stream) { + navigator.mediaDevices.enumerateDevices().then(function(devices) { + Janus.debug(devices); + callback(devices); + // Get rid of the now useless stream + Janus.stopAllTracks(stream) + }); + }) + .catch(function(err) { + Janus.error(err); + callback([]); + }); + } else { + Janus.warn("navigator.mediaDevices unavailable"); + callback([]); + } + }; + // Helper methods to attach/reattach a stream to a video element (previously part of adapter.js) + Janus.attachMediaStream = function(element, stream) { + try { + element.srcObject = stream; + } catch (e) { + try { + element.src = URL.createObjectURL(stream); + } catch (e) { + Janus.error("Error attaching stream to element"); + } + } + }; + Janus.reattachMediaStream = function(to, from) { + try { + to.srcObject = from.srcObject; + } catch (e) { + try { + to.src = from.src; + } catch (e) { + Janus.error("Error reattaching stream to element"); + } + } + }; + // Detect tab close: make sure we don't loose existing onbeforeunload handlers + // (note: for iOS we need to subscribe to a different event, 'pagehide', see + // https://gist.github.com/thehunmonkgroup/6bee8941a49b86be31a787fe8f4b8cfe) + var iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0; + var eventName = iOS ? 'pagehide' : 'beforeunload'; + var oldOBF = window["on" + eventName]; + window.addEventListener(eventName, function() { + Janus.log("Closing window"); + for(var s in Janus.sessions) { + if(Janus.sessions[s] && Janus.sessions[s].destroyOnUnload) { + Janus.log("Destroying session " + s); + Janus.sessions[s].destroy({unload: true, notifyDestroyed: false}); + } + } + if(oldOBF && typeof oldOBF == "function") { + oldOBF(); + } + }); + // If this is a Safari, check if VP8 or VP9 are supported + Janus.safariVp8 = false; + Janus.safariVp9 = false; + if(Janus.webRTCAdapter.browserDetails.browser === 'safari' && + Janus.webRTCAdapter.browserDetails.version >= 605) { + // Let's see if RTCRtpSender.getCapabilities() is there + if(RTCRtpSender && RTCRtpSender.getCapabilities && RTCRtpSender.getCapabilities("video") && + RTCRtpSender.getCapabilities("video").codecs && RTCRtpSender.getCapabilities("video").codecs.length) { + for(var codec of RTCRtpSender.getCapabilities("video").codecs) { + if(codec && codec.mimeType && codec.mimeType.toLowerCase() === "video/vp8") { + Janus.safariVp8 = true; + } else if(codec && codec.mimeType && codec.mimeType.toLowerCase() === "video/vp9") { + Janus.safariVp9 = true; + } + } + if(Janus.safariVp8) { + Janus.log("This version of Safari supports VP8"); + } else { + Janus.warn("This version of Safari does NOT support VP8: if you're using a Technology Preview, " + + "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu"); + } + } else { + // We do it in a very ugly way, as there's no alternative... + // We create a PeerConnection to see if VP8 is in an offer + var testpc = new RTCPeerConnection({}); + testpc.createOffer({offerToReceiveVideo: true}).then(function(offer) { + Janus.safariVp8 = offer.sdp.indexOf("VP8") !== -1; + Janus.safariVp9 = offer.sdp.indexOf("VP9") !== -1; + if(Janus.safariVp8) { + Janus.log("This version of Safari supports VP8"); + } else { + Janus.warn("This version of Safari does NOT support VP8: if you're using a Technology Preview, " + + "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu"); + } + testpc.close(); + testpc = null; + }); + } + } + // Check if this browser supports Unified Plan and transceivers + // Based on https://codepen.io/anon/pen/ZqLwWV?editors=0010 + Janus.unifiedPlan = false; + if(Janus.webRTCAdapter.browserDetails.browser === 'firefox' && + Janus.webRTCAdapter.browserDetails.version >= 59) { + // Firefox definitely does, starting from version 59 + Janus.unifiedPlan = true; + } else if(Janus.webRTCAdapter.browserDetails.browser === 'chrome' && + Janus.webRTCAdapter.browserDetails.version >= 72) { + // Chrome does, but it's only usable from version 72 on + Janus.unifiedPlan = true; + } else if(!window.RTCRtpTransceiver || !('currentDirection' in RTCRtpTransceiver.prototype)) { + // Safari supports addTransceiver() but not Unified Plan when + // currentDirection is not defined (see codepen above). + Janus.unifiedPlan = false; + } else { + // Check if addTransceiver() throws an exception + var tempPc = new RTCPeerConnection(); + try { + tempPc.addTransceiver('audio'); + Janus.unifiedPlan = true; + } catch (e) {} + tempPc.close(); + } + Janus.initDone = true; + options.callback(); + } +}; + +// Helper method to check whether WebRTC is supported by this browser +Janus.isWebrtcSupported = function() { + return !!window.RTCPeerConnection; +}; +// Helper method to check whether devices can be accessed by this browser (e.g., not possible via plain HTTP) +Janus.isGetUserMediaAvailable = function() { + return navigator.mediaDevices && navigator.mediaDevices.getUserMedia; +}; + +// Helper method to create random identifiers (e.g., transaction) +Janus.randomString = function(len) { + var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + var randomString = ''; + for (var i = 0; i < len; i++) { + var randomPoz = Math.floor(Math.random() * charSet.length); + randomString += charSet.substring(randomPoz,randomPoz+1); + } + return randomString; +}; + +export function Janus(gatewayCallbacks) { + gatewayCallbacks = gatewayCallbacks || {}; + gatewayCallbacks.success = (typeof gatewayCallbacks.success == "function") ? gatewayCallbacks.success : Janus.noop; + gatewayCallbacks.error = (typeof gatewayCallbacks.error == "function") ? gatewayCallbacks.error : Janus.noop; + gatewayCallbacks.destroyed = (typeof gatewayCallbacks.destroyed == "function") ? gatewayCallbacks.destroyed : Janus.noop; + if(!Janus.initDone) { + gatewayCallbacks.error("Library not initialized"); + return {}; + } + if(!Janus.isWebrtcSupported()) { + gatewayCallbacks.error("WebRTC not supported by this browser"); + return {}; + } + Janus.log("Library initialized: " + Janus.initDone); + if(!gatewayCallbacks.server) { + gatewayCallbacks.error("Invalid server url"); + return {}; + } + var websockets = false; + var ws = null; + var wsHandlers = {}; + var wsKeepaliveTimeoutId = null; + var servers = null; + var serversIndex = 0; + var server = gatewayCallbacks.server; + if(Janus.isArray(server)) { + Janus.log("Multiple servers provided (" + server.length + "), will use the first that works"); + server = null; + servers = gatewayCallbacks.server; + Janus.debug(servers); + } else { + if(server.indexOf("ws") === 0) { + websockets = true; + Janus.log("Using WebSockets to contact Janus: " + server); + } else { + websockets = false; + Janus.log("Using REST API to contact Janus: " + server); + } + } + var iceServers = gatewayCallbacks.iceServers || [{urls: "stun:stun.l.google.com:19302"}]; + var iceTransportPolicy = gatewayCallbacks.iceTransportPolicy; + var bundlePolicy = gatewayCallbacks.bundlePolicy; + // Whether IPv6 candidates should be gathered + var ipv6Support = (gatewayCallbacks.ipv6 === true); + // Whether we should enable the withCredentials flag for XHR requests + var withCredentials = false; + if(gatewayCallbacks.withCredentials !== undefined && gatewayCallbacks.withCredentials !== null) + withCredentials = gatewayCallbacks.withCredentials === true; + // Optional max events + var maxev = 10; + if(gatewayCallbacks.max_poll_events !== undefined && gatewayCallbacks.max_poll_events !== null) + maxev = gatewayCallbacks.max_poll_events; + if(maxev < 1) + maxev = 1; + // Token to use (only if the token based authentication mechanism is enabled) + var token = null; + if(gatewayCallbacks.token !== undefined && gatewayCallbacks.token !== null) + token = gatewayCallbacks.token; + // API secret to use (only if the shared API secret is enabled) + var apisecret = null; + if(gatewayCallbacks.apisecret !== undefined && gatewayCallbacks.apisecret !== null) + apisecret = gatewayCallbacks.apisecret; + // Whether we should destroy this session when onbeforeunload is called + this.destroyOnUnload = true; + if(gatewayCallbacks.destroyOnUnload !== undefined && gatewayCallbacks.destroyOnUnload !== null) + this.destroyOnUnload = (gatewayCallbacks.destroyOnUnload === true); + // Some timeout-related values + var keepAlivePeriod = 25000; + if(gatewayCallbacks.keepAlivePeriod !== undefined && gatewayCallbacks.keepAlivePeriod !== null) + keepAlivePeriod = gatewayCallbacks.keepAlivePeriod; + if(isNaN(keepAlivePeriod)) + keepAlivePeriod = 25000; + var longPollTimeout = 60000; + if(gatewayCallbacks.longPollTimeout !== undefined && gatewayCallbacks.longPollTimeout !== null) + longPollTimeout = gatewayCallbacks.longPollTimeout; + if(isNaN(longPollTimeout)) + longPollTimeout = 60000; + + // overrides for default maxBitrate values for simulcasting + function getMaxBitrates(simulcastMaxBitrates) { + var maxBitrates = { + high: 900000, + medium: 300000, + low: 100000, + }; + + if (simulcastMaxBitrates !== undefined && simulcastMaxBitrates !== null) { + if (simulcastMaxBitrates.high) + maxBitrates.high = simulcastMaxBitrates.high; + if (simulcastMaxBitrates.medium) + maxBitrates.medium = simulcastMaxBitrates.medium; + if (simulcastMaxBitrates.low) + maxBitrates.low = simulcastMaxBitrates.low; + } + + return maxBitrates; + } + + var connected = false; + var sessionId = null; + var pluginHandles = {}; + var that = this; + var retries = 0; + var transactions = {}; + createSession(gatewayCallbacks); + + // Public methods + this.getServer = function() { return server; }; + this.isConnected = function() { return connected; }; + this.reconnect = function(callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + callbacks["reconnect"] = true; + createSession(callbacks); + }; + this.getSessionId = function() { return sessionId; }; + this.getInfo = function(callbacks) { getInfo(callbacks); }; + this.destroy = function(callbacks) { destroySession(callbacks); }; + this.attach = function(callbacks) { createHandle(callbacks); }; + + function eventHandler() { + if(sessionId == null) + return; + Janus.debug('Long poll...'); + if(!connected) { + Janus.warn("Is the server down? (connected=false)"); + return; + } + var longpoll = server + "/" + sessionId + "?rid=" + new Date().getTime(); + if(maxev) + longpoll = longpoll + "&maxev=" + maxev; + if(token) + longpoll = longpoll + "&token=" + encodeURIComponent(token); + if(apisecret) + longpoll = longpoll + "&apisecret=" + encodeURIComponent(apisecret); + Janus.httpAPICall(longpoll, { + verb: 'GET', + withCredentials: withCredentials, + success: handleEvent, + timeout: longPollTimeout, + error: function(textStatus, errorThrown) { + Janus.error(textStatus + ":", errorThrown); + retries++; + if(retries > 3) { + // Did we just lose the server? :-( + connected = false; + gatewayCallbacks.error("Lost connection to the server (is it down?)"); + return; + } + eventHandler(); + } + }); + } + + // Private event handler: this will trigger plugin callbacks, if set + function handleEvent(json, skipTimeout) { + retries = 0; + if(!websockets && sessionId !== undefined && sessionId !== null && skipTimeout !== true) + eventHandler(); + if(!websockets && Janus.isArray(json)) { + // We got an array: it means we passed a maxev > 1, iterate on all objects + for(var i=0; i data channel: ' + dcState); + if(dcState === 'open') { + // Any pending messages to send? + if(config.dataChannel[label].pending && config.dataChannel[label].pending.length > 0) { + Janus.log("Sending pending messages on <" + label + ">:", config.dataChannel[label].pending.length); + for(var data of config.dataChannel[label].pending) { + Janus.log("Sending data on data channel <" + label + ">"); + Janus.debug(data); + config.dataChannel[label].send(data); + } + config.dataChannel[label].pending = []; + } + // Notify the open data channel + pluginHandle.ondataopen(label, protocol); + } + }; + var onDataChannelError = function(error) { + Janus.error('Got error on data channel:', error); + // TODO + }; + if(!incoming) { + // FIXME Add options (ordered, maxRetransmits, etc.) + var dcoptions = config.dataChannelOptions; + if(dcprotocol) + dcoptions.protocol = dcprotocol; + config.dataChannel[dclabel] = config.pc.createDataChannel(dclabel, dcoptions); + } else { + // The channel was created by Janus + config.dataChannel[dclabel] = incoming; + } + config.dataChannel[dclabel].onmessage = onDataChannelMessage; + config.dataChannel[dclabel].onopen = onDataChannelStateChange; + config.dataChannel[dclabel].onclose = onDataChannelStateChange; + config.dataChannel[dclabel].onerror = onDataChannelError; + config.dataChannel[dclabel].pending = []; + if(pendingData) + config.dataChannel[dclabel].pending.push(pendingData); + } + + // Private method to send a data channel message + function sendData(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + var data = callbacks.text || callbacks.data; + if(!data) { + Janus.warn("Invalid data"); + callbacks.error("Invalid data"); + return; + } + var label = callbacks.label ? callbacks.label : Janus.dataChanDefaultLabel; + if(!config.dataChannel[label]) { + // Create new data channel and wait for it to open + createDataChannel(handleId, label, callbacks.protocol, false, data, callbacks.protocol); + callbacks.success(); + return; + } + if(config.dataChannel[label].readyState !== "open") { + config.dataChannel[label].pending.push(data); + callbacks.success(); + return; + } + Janus.log("Sending data on data channel <" + label + ">"); + Janus.debug(data); + config.dataChannel[label].send(data); + callbacks.success(); + } + + // Private method to send a DTMF tone + function sendDtmf(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + if(!config.dtmfSender) { + // Create the DTMF sender the proper way, if possible + if(config.pc) { + var senders = config.pc.getSenders(); + var audioSender = senders.find(function(sender) { + return sender.track && sender.track.kind === 'audio'; + }); + if(!audioSender) { + Janus.warn("Invalid DTMF configuration (no audio track)"); + callbacks.error("Invalid DTMF configuration (no audio track)"); + return; + } + config.dtmfSender = audioSender.dtmf; + if(config.dtmfSender) { + Janus.log("Created DTMF Sender"); + config.dtmfSender.ontonechange = function(tone) { Janus.debug("Sent DTMF tone: " + tone.tone); }; + } + } + if(!config.dtmfSender) { + Janus.warn("Invalid DTMF configuration"); + callbacks.error("Invalid DTMF configuration"); + return; + } + } + var dtmf = callbacks.dtmf; + if(!dtmf) { + Janus.warn("Invalid DTMF parameters"); + callbacks.error("Invalid DTMF parameters"); + return; + } + var tones = dtmf.tones; + if(!tones) { + Janus.warn("Invalid DTMF string"); + callbacks.error("Invalid DTMF string"); + return; + } + var duration = (typeof dtmf.duration === 'number') ? dtmf.duration : 500; // We choose 500ms as the default duration for a tone + var gap = (typeof dtmf.gap === 'number') ? dtmf.gap : 50; // We choose 50ms as the default gap between tones + Janus.debug("Sending DTMF string " + tones + " (duration " + duration + "ms, gap " + gap + "ms)"); + config.dtmfSender.insertDTMF(tones, duration, gap); + callbacks.success(); + } + + // Private method to destroy a plugin handle + function destroyHandle(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + var noRequest = (callbacks.noRequest === true); + Janus.log("Destroying handle " + handleId + " (only-locally=" + noRequest + ")"); + cleanupWebrtc(handleId); + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || pluginHandle.detached) { + // Plugin was already detached by Janus, calling detach again will return a handle not found error, so just exit here + delete pluginHandles[handleId]; + callbacks.success(); + return; + } + pluginHandle.detached = true; + if(noRequest) { + // We're only removing the handle locally + delete pluginHandles[handleId]; + callbacks.success(); + return; + } + if(!connected) { + Janus.warn("Is the server down? (connected=false)"); + callbacks.error("Is the server down? (connected=false)"); + return; + } + var request = { "janus": "detach", "transaction": Janus.randomString(12) }; + if(pluginHandle.token) + request["token"] = pluginHandle.token; + if(apisecret) + request["apisecret"] = apisecret; + if(websockets) { + request["session_id"] = sessionId; + request["handle_id"] = handleId; + ws.send(JSON.stringify(request)); + delete pluginHandles[handleId]; + callbacks.success(); + return; + } + Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, { + verb: 'POST', + withCredentials: withCredentials, + body: request, + success: function(json) { + Janus.log("Destroyed handle:"); + Janus.debug(json); + if(json["janus"] !== "success") { + Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME + } + delete pluginHandles[handleId]; + callbacks.success(); + }, + error: function(textStatus, errorThrown) { + Janus.error(textStatus + ":", errorThrown); // FIXME + // We cleanup anyway + delete pluginHandles[handleId]; + callbacks.success(); + } + }); + } + + // WebRTC stuff + function streamsDone(handleId, jsep, media, callbacks, stream) { + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + // Close all tracks if the given stream has been created internally + if(!callbacks.stream) { + Janus.stopAllTracks(stream); + } + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + Janus.debug("streamsDone:", stream); + if(stream) { + Janus.debug(" -- Audio tracks:", stream.getAudioTracks()); + Janus.debug(" -- Video tracks:", stream.getVideoTracks()); + } + // We're now capturing the new stream: check if we're updating or if it's a new thing + var addTracks = false; + if(!config.myStream || !media.update || (config.streamExternal && !media.replaceAudio && !media.replaceVideo)) { + config.myStream = stream; + addTracks = true; + } else { + // We only need to update the existing stream + if(((!media.update && isAudioSendEnabled(media)) || (media.update && (media.addAudio || media.replaceAudio))) && + stream.getAudioTracks() && stream.getAudioTracks().length) { + config.myStream.addTrack(stream.getAudioTracks()[0]); + if(Janus.unifiedPlan) { + // Use Transceivers + Janus.log((media.replaceAudio ? "Replacing" : "Adding") + " audio track:", stream.getAudioTracks()[0]); + var audioTransceiver = null; + const transceivers = config.pc.getTransceivers(); + if(transceivers && transceivers.length > 0) { + for(const t of transceivers) { + if((t.sender && t.sender.track && t.sender.track.kind === "audio") || + (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) { + audioTransceiver = t; + break; + } + } + } + if(audioTransceiver && audioTransceiver.sender) { + audioTransceiver.sender.replaceTrack(stream.getAudioTracks()[0]); + } else { + config.pc.addTrack(stream.getAudioTracks()[0], stream); + } + } else { + Janus.log((media.replaceAudio ? "Replacing" : "Adding") + " audio track:", stream.getAudioTracks()[0]); + config.pc.addTrack(stream.getAudioTracks()[0], stream); + } + } + if(((!media.update && isVideoSendEnabled(media)) || (media.update && (media.addVideo || media.replaceVideo))) && + stream.getVideoTracks() && stream.getVideoTracks().length) { + config.myStream.addTrack(stream.getVideoTracks()[0]); + if(Janus.unifiedPlan) { + // Use Transceivers + Janus.log((media.replaceVideo ? "Replacing" : "Adding") + " video track:", stream.getVideoTracks()[0]); + var videoTransceiver = null; + const transceivers = config.pc.getTransceivers(); + if(transceivers && transceivers.length > 0) { + for(const t of transceivers) { + if((t.sender && t.sender.track && t.sender.track.kind === "video") || + (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) { + videoTransceiver = t; + break; + } + } + } + if(videoTransceiver && videoTransceiver.sender) { + videoTransceiver.sender.replaceTrack(stream.getVideoTracks()[0]); + } else { + config.pc.addTrack(stream.getVideoTracks()[0], stream); + } + } else { + Janus.log((media.replaceVideo ? "Replacing" : "Adding") + " video track:", stream.getVideoTracks()[0]); + config.pc.addTrack(stream.getVideoTracks()[0], stream); + } + } + } + // If we still need to create a PeerConnection, let's do that + if(!config.pc) { + var pc_config = {"iceServers": iceServers, "iceTransportPolicy": iceTransportPolicy, "bundlePolicy": bundlePolicy}; + if(Janus.webRTCAdapter.browserDetails.browser === "chrome") { + // For Chrome versions before 72, we force a plan-b semantic, and unified-plan otherwise + pc_config["sdpSemantics"] = (Janus.webRTCAdapter.browserDetails.version < 72) ? "plan-b" : "unified-plan"; + } + var pc_constraints = {}; + if(Janus.webRTCAdapter.browserDetails.browser === "edge") { + // This is Edge, enable BUNDLE explicitly + pc_config.bundlePolicy = "max-bundle"; + } + // Check if a sender or receiver transform has been provided + if(RTCRtpSender && (RTCRtpSender.prototype.createEncodedStreams || + (RTCRtpSender.prototype.createEncodedAudioStreams && + RTCRtpSender.prototype.createEncodedVideoStreams)) && + (callbacks.senderTransforms || callbacks.receiverTransforms)) { + config.senderTransforms = callbacks.senderTransforms; + config.receiverTransforms = callbacks.receiverTransforms; + pc_config["forceEncodedAudioInsertableStreams"] = true; + pc_config["forceEncodedVideoInsertableStreams"] = true; + pc_config["encodedInsertableStreams"] = true; + } + Janus.log("Creating PeerConnection"); + Janus.debug(pc_constraints); + config.pc = new RTCPeerConnection(pc_config, pc_constraints); + Janus.debug(config.pc); + if(config.pc.getStats) { // FIXME + config.volume = {}; + config.bitrate.value = "0 kbits/sec"; + } + Janus.log("Preparing local SDP and gathering candidates (trickle=" + config.trickle + ")"); + config.pc.oniceconnectionstatechange = function() { + if(config.pc) + pluginHandle.iceState(config.pc.iceConnectionState); + }; + config.pc.onicecandidate = function(event) { + if (!event.candidate || + (Janus.webRTCAdapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) { + Janus.log("End of candidates."); + config.iceDone = true; + if(config.trickle === true) { + // Notify end of candidates + sendTrickleCandidate(handleId, {"completed": true}); + } else { + // No trickle, time to send the complete SDP (including all candidates) + sendSDP(handleId, callbacks); + } + } else { + // JSON.stringify doesn't work on some WebRTC objects anymore + // See https://code.google.com/p/chromium/issues/detail?id=467366 + var candidate = { + "candidate": event.candidate.candidate, + "sdpMid": event.candidate.sdpMid, + "sdpMLineIndex": event.candidate.sdpMLineIndex + }; + if(config.trickle === true) { + // Send candidate + sendTrickleCandidate(handleId, candidate); + } + } + }; + config.pc.ontrack = function(event) { + Janus.log("Handling Remote Track"); + Janus.debug(event); + if(!event.streams) + return; + config.remoteStream = event.streams[0]; + pluginHandle.onremotestream(config.remoteStream); + if(event.track.onended) + return; + if(config.receiverTransforms) { + var receiverStreams = null; + if(RTCRtpSender.prototype.createEncodedStreams) { + receiverStreams = event.receiver.createEncodedStreams(); + } else if(RTCRtpSender.prototype.createAudioEncodedStreams || RTCRtpSender.prototype.createEncodedVideoStreams) { + if(event.track.kind === "audio" && config.receiverTransforms["audio"]) { + receiverStreams = event.receiver.createEncodedAudioStreams(); + } else if(event.track.kind === "video" && config.receiverTransforms["video"]) { + receiverStreams = event.receiver.createEncodedVideoStreams(); + } + } + if(receiverStreams) { + console.log(receiverStreams); + if(receiverStreams.readableStream && receiverStreams.writableStream) { + receiverStreams.readableStream + .pipeThrough(config.receiverTransforms[event.track.kind]) + .pipeTo(receiverStreams.writableStream); + } else if(receiverStreams.readable && receiverStreams.writable) { + receiverStreams.readable + .pipeThrough(config.receiverTransforms[event.track.kind]) + .pipeTo(receiverStreams.writable); + } + } + } + var trackMutedTimeoutId = null; + Janus.log("Adding onended callback to track:", event.track); + event.track.onended = function(ev) { + Janus.log("Remote track removed:", ev); + if(config.remoteStream) { + clearTimeout(trackMutedTimeoutId); + config.remoteStream.removeTrack(ev.target); + pluginHandle.onremotestream(config.remoteStream); + } + }; + event.track.onmute = function(ev) { + Janus.log("Remote track muted:", ev); + if(config.remoteStream && trackMutedTimeoutId == null) { + trackMutedTimeoutId = setTimeout(function() { + Janus.log("Removing remote track"); + if (config.remoteStream) { + config.remoteStream.removeTrack(ev.target); + pluginHandle.onremotestream(config.remoteStream); + } + trackMutedTimeoutId = null; + // Chrome seems to raise mute events only at multiples of 834ms; + // we set the timeout to three times this value (rounded to 840ms) + }, 3 * 840); + } + }; + event.track.onunmute = function(ev) { + Janus.log("Remote track flowing again:", ev); + if(trackMutedTimeoutId != null) { + clearTimeout(trackMutedTimeoutId); + trackMutedTimeoutId = null; + } else { + try { + config.remoteStream.addTrack(ev.target); + pluginHandle.onremotestream(config.remoteStream); + } catch(e) { + Janus.error(e); + } + } + }; + }; + } + if(addTracks && stream) { + Janus.log('Adding local stream'); + var simulcast = (callbacks.simulcast === true || callbacks.simulcast2 === true) && Janus.unifiedPlan; + var svc = callbacks.svc; + stream.getTracks().forEach(function(track) { + Janus.log('Adding local track:', track); + var sender = null; + if((!simulcast && !svc) || track.kind === 'audio') { + sender = config.pc.addTrack(track, stream); + } else if(simulcast) { + Janus.log('Enabling rid-based simulcasting:', track); + let maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates); + let tr = config.pc.addTransceiver(track, { + direction: "sendrecv", + streams: [stream], + sendEncodings: callbacks.sendEncodings || [ + { rid: "h", active: true, maxBitrate: maxBitrates.high }, + { rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2 }, + { rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4 } + ] + }); + if(tr) + sender = tr.sender; + } else { + Janus.log('Enabling SVC (' + svc + '):', track); + let tr = config.pc.addTransceiver(track, { + direction: "sendrecv", + streams: [stream], + sendEncodings: [ + { scalabilityMode: svc } + ] + }); + if(tr) + sender = tr.sender; + } + // Check if insertable streams are involved + if(sender && config.senderTransforms) { + var senderStreams = null; + if(RTCRtpSender.prototype.createEncodedStreams) { + senderStreams = sender.createEncodedStreams(); + } else if(RTCRtpSender.prototype.createAudioEncodedStreams || RTCRtpSender.prototype.createEncodedVideoStreams) { + if(sender.track.kind === "audio" && config.senderTransforms["audio"]) { + senderStreams = sender.createEncodedAudioStreams(); + } else if(sender.track.kind === "video" && config.senderTransforms["video"]) { + senderStreams = sender.createEncodedVideoStreams(); + } + } + if(senderStreams) { + console.log(senderStreams); + if(senderStreams.readableStream && senderStreams.writableStream) { + senderStreams.readableStream + .pipeThrough(config.senderTransforms[sender.track.kind]) + .pipeTo(senderStreams.writableStream); + } else if(senderStreams.readable && senderStreams.writable) { + senderStreams.readable + .pipeThrough(config.senderTransforms[sender.track.kind]) + .pipeTo(senderStreams.writable); + } + } + } + }); + } + // Any data channel to create? + if(isDataEnabled(media) && !config.dataChannel[Janus.dataChanDefaultLabel]) { + Janus.log("Creating default data channel"); + createDataChannel(handleId, Janus.dataChanDefaultLabel, null, false); + config.pc.ondatachannel = function(event) { + Janus.log("Data channel created by Janus:", event); + createDataChannel(handleId, event.channel.label, event.channel.protocol, event.channel); + }; + } + // If there's a new local stream, let's notify the application + if(config.myStream) { + pluginHandle.onlocalstream(config.myStream); + } + // Create offer/answer now + if(!jsep) { + createOffer(handleId, media, callbacks); + } else { + config.pc.setRemoteDescription(jsep) + .then(function() { + Janus.log("Remote description accepted!"); + config.remoteSdp = jsep.sdp; + // Any trickle candidate we cached? + if(config.candidates && config.candidates.length > 0) { + for(var i = 0; i< config.candidates.length; i++) { + var candidate = config.candidates[i]; + Janus.debug("Adding remote candidate:", candidate); + if(!candidate || candidate.completed === true) { + // end-of-candidates + config.pc.addIceCandidate(Janus.endOfCandidates); + } else { + // New candidate + config.pc.addIceCandidate(candidate); + } + } + config.candidates = []; + } + // Create the answer now + createAnswer(handleId, media, callbacks); + }, callbacks.error); + } + } + + function prepareWebrtc(handleId, offer, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError; + var jsep = callbacks.jsep; + if(offer && jsep) { + Janus.error("Provided a JSEP to a createOffer"); + callbacks.error("Provided a JSEP to a createOffer"); + return; + } else if(!offer && (!jsep || !jsep.type || !jsep.sdp)) { + Janus.error("A valid JSEP is required for createAnswer"); + callbacks.error("A valid JSEP is required for createAnswer"); + return; + } + /* Check that callbacks.media is a (not null) Object */ + callbacks.media = (typeof callbacks.media === 'object' && callbacks.media) ? callbacks.media : { audio: true, video: true }; + var media = callbacks.media; + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + config.trickle = isTrickleEnabled(callbacks.trickle); + // Are we updating a session? + if(!config.pc) { + // Nope, new PeerConnection + media.update = false; + media.keepAudio = false; + media.keepVideo = false; + } else { + Janus.log("Updating existing media session"); + media.update = true; + // Check if there's anything to add/remove/replace, or if we + // can go directly to preparing the new SDP offer or answer + if(callbacks.stream) { + // External stream: is this the same as the one we were using before? + if(callbacks.stream !== config.myStream) { + Janus.log("Renegotiation involves a new external stream"); + } + } else { + // Check if there are changes on audio + if(media.addAudio) { + media.keepAudio = false; + media.replaceAudio = false; + media.removeAudio = false; + media.audioSend = true; + if(config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) { + Janus.error("Can't add audio stream, there already is one"); + callbacks.error("Can't add audio stream, there already is one"); + return; + } + } else if(media.removeAudio) { + media.keepAudio = false; + media.replaceAudio = false; + media.addAudio = false; + media.audioSend = false; + } else if(media.replaceAudio) { + media.keepAudio = false; + media.addAudio = false; + media.removeAudio = false; + media.audioSend = true; + } + if(!config.myStream) { + // No media stream: if we were asked to replace, it's actually an "add" + if(media.replaceAudio) { + media.keepAudio = false; + media.replaceAudio = false; + media.addAudio = true; + media.audioSend = true; + } + if(isAudioSendEnabled(media)) { + media.keepAudio = false; + media.addAudio = true; + } + } else { + if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { + // No audio track: if we were asked to replace, it's actually an "add" + if(media.replaceAudio) { + media.keepAudio = false; + media.replaceAudio = false; + media.addAudio = true; + media.audioSend = true; + } + if(isAudioSendEnabled(media)) { + media.keepAudio = false; + media.addAudio = true; + } + } else { + // We have an audio track: should we keep it as it is? + if(isAudioSendEnabled(media) && + !media.removeAudio && !media.replaceAudio) { + media.keepAudio = true; + } + } + } + // Check if there are changes on video + if(media.addVideo) { + media.keepVideo = false; + media.replaceVideo = false; + media.removeVideo = false; + media.videoSend = true; + if(config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) { + Janus.error("Can't add video stream, there already is one"); + callbacks.error("Can't add video stream, there already is one"); + return; + } + } else if(media.removeVideo) { + media.keepVideo = false; + media.replaceVideo = false; + media.addVideo = false; + media.videoSend = false; + } else if(media.replaceVideo) { + media.keepVideo = false; + media.addVideo = false; + media.removeVideo = false; + media.videoSend = true; + } + if(!config.myStream) { + // No media stream: if we were asked to replace, it's actually an "add" + if(media.replaceVideo) { + media.keepVideo = false; + media.replaceVideo = false; + media.addVideo = true; + media.videoSend = true; + } + if(isVideoSendEnabled(media)) { + media.keepVideo = false; + media.addVideo = true; + } + } else { + if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { + // No video track: if we were asked to replace, it's actually an "add" + if(media.replaceVideo) { + media.keepVideo = false; + media.replaceVideo = false; + media.addVideo = true; + media.videoSend = true; + } + if(isVideoSendEnabled(media)) { + media.keepVideo = false; + media.addVideo = true; + } + } else { + // We have a video track: should we keep it as it is? + if(isVideoSendEnabled(media) && !media.removeVideo && !media.replaceVideo) { + media.keepVideo = true; + } + } + } + // Data channels can only be added + if(media.addData) { + media.data = true; + } + } + // If we're updating and keeping all tracks, let's skip the getUserMedia part + if((isAudioSendEnabled(media) && media.keepAudio) && + (isVideoSendEnabled(media) && media.keepVideo)) { + pluginHandle.consentDialog(false); + streamsDone(handleId, jsep, media, callbacks, config.myStream); + return; + } + } + // If we're updating, check if we need to remove/replace one of the tracks + if(media.update && (!config.streamExternal || (config.streamExternal && (media.replaceAudio || media.replaceVideo)))) { + if(media.removeAudio || media.replaceAudio) { + if(config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) { + var at = config.myStream.getAudioTracks()[0]; + Janus.log("Removing audio track:", at); + config.myStream.removeTrack(at); + try { + at.stop(); + } catch(e) {} + } + if(config.pc.getSenders() && config.pc.getSenders().length) { + var ra = true; + if(media.replaceAudio && Janus.unifiedPlan) { + // We can use replaceTrack + ra = false; + } + if(ra) { + for(var asnd of config.pc.getSenders()) { + if(asnd && asnd.track && asnd.track.kind === "audio") { + Janus.log("Removing audio sender:", asnd); + config.pc.removeTrack(asnd); + } + } + } + } + } + if(media.removeVideo || media.replaceVideo) { + if(config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) { + var vt = config.myStream.getVideoTracks()[0]; + Janus.log("Removing video track:", vt); + config.myStream.removeTrack(vt); + try { + vt.stop(); + } catch(e) {} + } + if(config.pc.getSenders() && config.pc.getSenders().length) { + var rv = true; + if(media.replaceVideo && Janus.unifiedPlan) { + // We can use replaceTrack + rv = false; + } + if(rv) { + for(var vsnd of config.pc.getSenders()) { + if(vsnd && vsnd.track && vsnd.track.kind === "video") { + Janus.log("Removing video sender:", vsnd); + config.pc.removeTrack(vsnd); + } + } + } + } + } + } + // Was a MediaStream object passed, or do we need to take care of that? + if(callbacks.stream) { + var stream = callbacks.stream; + Janus.log("MediaStream provided by the application"); + Janus.debug(stream); + // If this is an update, let's check if we need to release the previous stream + if(media.update && config.myStream && config.myStream !== callbacks.stream && !config.streamExternal && !media.replaceAudio && !media.replaceVideo) { + // We're replacing a stream we captured ourselves with an external one + Janus.stopAllTracks(config.myStream); + config.myStream = null; + } + // Skip the getUserMedia part + config.streamExternal = true; + pluginHandle.consentDialog(false); + streamsDone(handleId, jsep, media, callbacks, stream); + return; + } + if(isAudioSendEnabled(media) || isVideoSendEnabled(media)) { + if(!Janus.isGetUserMediaAvailable()) { + callbacks.error("getUserMedia not available"); + return; + } + var constraints = { mandatory: {}, optional: []}; + pluginHandle.consentDialog(true); + var audioSupport = isAudioSendEnabled(media); + if(audioSupport && media && typeof media.audio === 'object') + audioSupport = media.audio; + var videoSupport = isVideoSendEnabled(media); + if(videoSupport && media) { + var simulcast = (callbacks.simulcast === true || callbacks.simulcast2 === true); + var svc = callbacks.svc; + if((simulcast || svc) && !jsep && !media.video) + media.video = "hires"; + if(media.video && media.video != 'screen' && media.video != 'window') { + if(typeof media.video === 'object') { + videoSupport = media.video; + } else { + var width = 0; + var height = 0; + if(media.video === 'lowres') { + // Small resolution, 4:3 + height = 240; + width = 320; + } else if(media.video === 'lowres-16:9') { + // Small resolution, 16:9 + height = 180; + width = 320; + } else if(media.video === 'hires' || media.video === 'hires-16:9' || media.video === 'hdres') { + // High(HD) resolution is only 16:9 + height = 720; + width = 1280; + } else if(media.video === 'fhdres') { + // Full HD resolution is only 16:9 + height = 1080; + width = 1920; + } else if(media.video === '4kres') { + // 4K resolution is only 16:9 + height = 2160; + width = 3840; + } else if(media.video === 'stdres') { + // Normal resolution, 4:3 + height = 480; + width = 640; + } else if(media.video === 'stdres-16:9') { + // Normal resolution, 16:9 + height = 360; + width = 640; + } else { + Janus.log("Default video setting is stdres 4:3"); + height = 480; + width = 640; + } + Janus.log("Adding media constraint:", media.video); + videoSupport = { + 'height': {'ideal': height}, + 'width': {'ideal': width} + }; + Janus.log("Adding video constraint:", videoSupport); + } + } else if(media.video === 'screen' || media.video === 'window') { + if(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) { + // The new experimental getDisplayMedia API is available, let's use that + // https://groups.google.com/forum/#!topic/discuss-webrtc/Uf0SrR4uxzk + // https://webrtchacks.com/chrome-screensharing-getdisplaymedia/ + constraints.video = {}; + if(media.screenshareFrameRate) { + constraints.video.frameRate = media.screenshareFrameRate; + } + if(media.screenshareHeight) { + constraints.video.height = media.screenshareHeight; + } + if(media.screenshareWidth) { + constraints.video.width = media.screenshareWidth; + } + constraints.audio = media.captureDesktopAudio; + navigator.mediaDevices.getDisplayMedia(constraints) + .then(function(stream) { + pluginHandle.consentDialog(false); + if(isAudioSendEnabled(media) && !media.keepAudio) { + navigator.mediaDevices.getUserMedia({ audio: true, video: false }) + .then(function (audioStream) { + stream.addTrack(audioStream.getAudioTracks()[0]); + streamsDone(handleId, jsep, media, callbacks, stream); + }) + } else { + streamsDone(handleId, jsep, media, callbacks, stream); + } + }, function (error) { + pluginHandle.consentDialog(false); + callbacks.error(error); + }); + return; + } + // We're going to try and use the extension for Chrome 34+, the old approach + // for older versions of Chrome, or the experimental support in Firefox 33+ + const callbackUserMedia = function(error, stream) { + pluginHandle.consentDialog(false); + if(error) { + callbacks.error(error); + } else { + streamsDone(handleId, jsep, media, callbacks, stream); + } + } + const getScreenMedia = function(constraints, gsmCallback, useAudio) { + Janus.log("Adding media constraint (screen capture)"); + Janus.debug(constraints); + navigator.mediaDevices.getUserMedia(constraints) + .then(function(stream) { + if(useAudio) { + navigator.mediaDevices.getUserMedia({ audio: true, video: false }) + .then(function (audioStream) { + stream.addTrack(audioStream.getAudioTracks()[0]); + gsmCallback(null, stream); + }) + } else { + gsmCallback(null, stream); + } + }) + .catch(function(error) { pluginHandle.consentDialog(false); gsmCallback(error); }); + } + if(Janus.webRTCAdapter.browserDetails.browser === 'chrome') { + var chromever = Janus.webRTCAdapter.browserDetails.version; + var maxver = 33; + if(window.navigator.userAgent.match('Linux')) + maxver = 35; // "known" crash in chrome 34 and 35 on linux + if(chromever >= 26 && chromever <= maxver) { + // Chrome 26->33 requires some awkward chrome://flags manipulation + constraints = { + video: { + mandatory: { + googLeakyBucket: true, + maxWidth: window.screen.width, + maxHeight: window.screen.height, + minFrameRate: media.screenshareFrameRate, + maxFrameRate: media.screenshareFrameRate, + chromeMediaSource: 'screen' + } + }, + audio: isAudioSendEnabled(media) && !media.keepAudio + }; + getScreenMedia(constraints, callbackUserMedia); + } else { + // Chrome 34+ requires an extension + Janus.extension.getScreen(function (error, sourceId) { + if (error) { + pluginHandle.consentDialog(false); + return callbacks.error(error); + } + constraints = { + audio: false, + video: { + mandatory: { + chromeMediaSource: 'desktop', + maxWidth: window.screen.width, + maxHeight: window.screen.height, + minFrameRate: media.screenshareFrameRate, + maxFrameRate: media.screenshareFrameRate, + }, + optional: [ + {googLeakyBucket: true}, + {googTemporalLayeredScreencast: true} + ] + } + }; + constraints.video.mandatory.chromeMediaSourceId = sourceId; + getScreenMedia(constraints, callbackUserMedia, + isAudioSendEnabled(media) && !media.keepAudio); + }); + } + } else if(Janus.webRTCAdapter.browserDetails.browser === 'firefox') { + if(Janus.webRTCAdapter.browserDetails.version >= 33) { + // Firefox 33+ has experimental support for screen sharing + constraints = { + video: { + mozMediaSource: media.video, + mediaSource: media.video + }, + audio: isAudioSendEnabled(media) && !media.keepAudio + }; + getScreenMedia(constraints, function (err, stream) { + callbackUserMedia(err, stream); + // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810 + if (!err) { + var lastTime = stream.currentTime; + var polly = window.setInterval(function () { + if(!stream) + window.clearInterval(polly); + if(stream.currentTime == lastTime) { + window.clearInterval(polly); + if(stream.onended) { + stream.onended(); + } + } + lastTime = stream.currentTime; + }, 500); + } + }); + } else { + var error = new Error('NavigatorUserMediaError'); + error.name = 'Your version of Firefox does not support screen sharing, please install Firefox 33 (or more recent versions)'; + pluginHandle.consentDialog(false); + callbacks.error(error); + return; + } + } + return; + } + } + // If we got here, we're not screensharing + if(!media || media.video !== 'screen') { + // Check whether all media sources are actually available or not + navigator.mediaDevices.enumerateDevices().then(function(devices) { + var audioExist = devices.some(function(device) { + return device.kind === 'audioinput'; + }), + videoExist = isScreenSendEnabled(media) || devices.some(function(device) { + return device.kind === 'videoinput'; + }); + + // Check whether a missing device is really a problem + var audioSend = isAudioSendEnabled(media); + var videoSend = isVideoSendEnabled(media); + var needAudioDevice = isAudioSendRequired(media); + var needVideoDevice = isVideoSendRequired(media); + if(audioSend || videoSend || needAudioDevice || needVideoDevice) { + // We need to send either audio or video + var haveAudioDevice = audioSend ? audioExist : false; + var haveVideoDevice = videoSend ? videoExist : false; + if(!haveAudioDevice && !haveVideoDevice) { + // FIXME Should we really give up, or just assume recvonly for both? + pluginHandle.consentDialog(false); + callbacks.error('No capture device found'); + return false; + } else if(!haveAudioDevice && needAudioDevice) { + pluginHandle.consentDialog(false); + callbacks.error('Audio capture is required, but no capture device found'); + return false; + } else if(!haveVideoDevice && needVideoDevice) { + pluginHandle.consentDialog(false); + callbacks.error('Video capture is required, but no capture device found'); + return false; + } + } + + var gumConstraints = { + audio: (audioExist && !media.keepAudio) ? audioSupport : false, + video: (videoExist && !media.keepVideo) ? videoSupport : false + }; + Janus.debug("getUserMedia constraints", gumConstraints); + if (!gumConstraints.audio && !gumConstraints.video) { + pluginHandle.consentDialog(false); + streamsDone(handleId, jsep, media, callbacks, stream); + } else { + navigator.mediaDevices.getUserMedia(gumConstraints) + .then(function(stream) { + pluginHandle.consentDialog(false); + streamsDone(handleId, jsep, media, callbacks, stream); + }).catch(function(error) { + pluginHandle.consentDialog(false); + callbacks.error({code: error.code, name: error.name, message: error.message}); + }); + } + }) + .catch(function(error) { + pluginHandle.consentDialog(false); + callbacks.error(error); + }); + } + } else { + // No need to do a getUserMedia, create offer/answer right away + streamsDone(handleId, jsep, media, callbacks); + } + } + + function prepareWebrtcPeer(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError; + callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop; + var jsep = callbacks.jsep; + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + if(jsep) { + if(!config.pc) { + Janus.warn("Wait, no PeerConnection?? if this is an answer, use createAnswer and not handleRemoteJsep"); + callbacks.error("No PeerConnection: if this is an answer, use createAnswer and not handleRemoteJsep"); + return; + } + callbacks.customizeSdp(jsep); + config.pc.setRemoteDescription(jsep) + .then(function() { + Janus.log("Remote description accepted!"); + config.remoteSdp = jsep.sdp; + // Any trickle candidate we cached? + if(config.candidates && config.candidates.length > 0) { + for(var i = 0; i< config.candidates.length; i++) { + var candidate = config.candidates[i]; + Janus.debug("Adding remote candidate:", candidate); + if(!candidate || candidate.completed === true) { + // end-of-candidates + config.pc.addIceCandidate(Janus.endOfCandidates); + } else { + // New candidate + config.pc.addIceCandidate(candidate); + } + } + config.candidates = []; + } + // Done + callbacks.success(); + }, callbacks.error); + } else { + callbacks.error("Invalid JSEP"); + } + } + + function createOffer(handleId, media, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop; + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + var simulcast = (callbacks.simulcast === true); + if(!simulcast) { + Janus.log("Creating offer (iceDone=" + config.iceDone + ")"); + } else { + Janus.log("Creating offer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")"); + } + // https://code.google.com/p/webrtc/issues/detail?id=3508 + var mediaConstraints = {}; + if(Janus.unifiedPlan) { + // We can use Transceivers + var audioTransceiver = null, videoTransceiver = null; + var transceivers = config.pc.getTransceivers(); + if(transceivers && transceivers.length > 0) { + for(var t of transceivers) { + if((t.sender && t.sender.track && t.sender.track.kind === "audio") || + (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) { + if(!audioTransceiver) { + audioTransceiver = t; + } + continue; + } + if((t.sender && t.sender.track && t.sender.track.kind === "video") || + (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) { + if(!videoTransceiver) { + videoTransceiver = t; + } + continue; + } + } + } + // Handle audio (and related changes, if any) + var audioSend = isAudioSendEnabled(media); + var audioRecv = isAudioRecvEnabled(media); + if(!audioSend && !audioRecv) { + // Audio disabled: have we removed it? + if(media.removeAudio && audioTransceiver) { + if (audioTransceiver.setDirection) { + audioTransceiver.setDirection("inactive"); + } else { + audioTransceiver.direction = "inactive"; + } + Janus.log("Setting audio transceiver to inactive:", audioTransceiver); + } + } else { + // Take care of audio m-line + if(audioSend && audioRecv) { + if(audioTransceiver) { + if (audioTransceiver.setDirection) { + audioTransceiver.setDirection("sendrecv"); + } else { + audioTransceiver.direction = "sendrecv"; + } + Janus.log("Setting audio transceiver to sendrecv:", audioTransceiver); + } + } else if(audioSend && !audioRecv) { + if(audioTransceiver) { + if (audioTransceiver.setDirection) { + audioTransceiver.setDirection("sendonly"); + } else { + audioTransceiver.direction = "sendonly"; + } + Janus.log("Setting audio transceiver to sendonly:", audioTransceiver); + } + } else if(!audioSend && audioRecv) { + if(audioTransceiver) { + if (audioTransceiver.setDirection) { + audioTransceiver.setDirection("recvonly"); + } else { + audioTransceiver.direction = "recvonly"; + } + Janus.log("Setting audio transceiver to recvonly:", audioTransceiver); + } else { + // In theory, this is the only case where we might not have a transceiver yet + audioTransceiver = config.pc.addTransceiver("audio", { direction: "recvonly" }); + Janus.log("Adding recvonly audio transceiver:", audioTransceiver); + } + } + } + // Handle video (and related changes, if any) + var videoSend = isVideoSendEnabled(media); + var videoRecv = isVideoRecvEnabled(media); + if(!videoSend && !videoRecv) { + // Video disabled: have we removed it? + if(media.removeVideo && videoTransceiver) { + if (videoTransceiver.setDirection) { + videoTransceiver.setDirection("inactive"); + } else { + videoTransceiver.direction = "inactive"; + } + Janus.log("Setting video transceiver to inactive:", videoTransceiver); + } + } else { + // Take care of video m-line + if(videoSend && videoRecv) { + if(videoTransceiver) { + if (videoTransceiver.setDirection) { + videoTransceiver.setDirection("sendrecv"); + } else { + videoTransceiver.direction = "sendrecv"; + } + Janus.log("Setting video transceiver to sendrecv:", videoTransceiver); + } + } else if(videoSend && !videoRecv) { + if(videoTransceiver) { + if (videoTransceiver.setDirection) { + videoTransceiver.setDirection("sendonly"); + } else { + videoTransceiver.direction = "sendonly"; + } + Janus.log("Setting video transceiver to sendonly:", videoTransceiver); + } + } else if(!videoSend && videoRecv) { + if(videoTransceiver) { + if (videoTransceiver.setDirection) { + videoTransceiver.setDirection("recvonly"); + } else { + videoTransceiver.direction = "recvonly"; + } + Janus.log("Setting video transceiver to recvonly:", videoTransceiver); + } else { + // In theory, this is the only case where we might not have a transceiver yet + videoTransceiver = config.pc.addTransceiver("video", { direction: "recvonly" }); + Janus.log("Adding recvonly video transceiver:", videoTransceiver); + } + } + } + } else { + mediaConstraints["offerToReceiveAudio"] = isAudioRecvEnabled(media); + mediaConstraints["offerToReceiveVideo"] = isVideoRecvEnabled(media); + } + var iceRestart = (callbacks.iceRestart === true); + if(iceRestart) { + mediaConstraints["iceRestart"] = true; + } + Janus.debug(mediaConstraints); + // Check if this is Firefox and we've been asked to do simulcasting + var sendVideo = isVideoSendEnabled(media); + if(sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === "firefox") { + // FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b + Janus.log("Enabling Simulcasting for Firefox (RID)"); + var sender = config.pc.getSenders().find(function(s) {return s.track && s.track.kind === "video"}); + if(sender) { + var parameters = sender.getParameters(); + if(!parameters) { + parameters = {}; + } + var maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates); + parameters.encodings = callbacks.sendEncodings || [ + { rid: "h", active: true, maxBitrate: maxBitrates.high }, + { rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2 }, + { rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4 } + ]; + sender.setParameters(parameters); + } + } + config.pc.createOffer(mediaConstraints) + .then(function(offer) { + Janus.debug(offer); + // JSON.stringify doesn't work on some WebRTC objects anymore + // See https://code.google.com/p/chromium/issues/detail?id=467366 + var jsep = { + "type": offer.type, + "sdp": offer.sdp + }; + callbacks.customizeSdp(jsep); + offer.sdp = jsep.sdp; + Janus.log("Setting local description"); + if(sendVideo && simulcast && !Janus.unifiedPlan) { + // We only do simulcast via SDP munging on older versions of Chrome and Safari + if(Janus.webRTCAdapter.browserDetails.browser === "chrome" || + Janus.webRTCAdapter.browserDetails.browser === "safari") { + Janus.log("Enabling Simulcasting for Chrome (SDP munging)"); + offer.sdp = mungeSdpForSimulcasting(offer.sdp); + } + } + config.mySdp = { + type: "offer", + sdp: offer.sdp + }; + config.pc.setLocalDescription(offer) + .catch(callbacks.error); + config.mediaConstraints = mediaConstraints; + if(!config.iceDone && !config.trickle) { + // Don't do anything until we have all candidates + Janus.log("Waiting for all candidates..."); + return; + } + // If transforms are present, notify Janus that the media is end-to-end encrypted + if(config.senderTransforms || config.receiverTransforms) { + offer["e2ee"] = true; + } + callbacks.success(offer); + }, callbacks.error); + } + + function createAnswer(handleId, media, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop; + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + var simulcast = (callbacks.simulcast === true || callbacks.simulcast2 === true); + if(!simulcast) { + Janus.log("Creating answer (iceDone=" + config.iceDone + ")"); + } else { + Janus.log("Creating answer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")"); + } + var mediaConstraints = null; + if(Janus.unifiedPlan) { + // We can use Transceivers + mediaConstraints = {}; + var audioTransceiver = null, videoTransceiver = null; + var transceivers = config.pc.getTransceivers(); + if(transceivers && transceivers.length > 0) { + for(var t of transceivers) { + if((t.sender && t.sender.track && t.sender.track.kind === "audio") || + (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) { + if(!audioTransceiver) + audioTransceiver = t; + continue; + } + if((t.sender && t.sender.track && t.sender.track.kind === "video") || + (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) { + if(!videoTransceiver) + videoTransceiver = t; + continue; + } + } + } + // Handle audio (and related changes, if any) + var audioSend = isAudioSendEnabled(media); + var audioRecv = isAudioRecvEnabled(media); + if(!audioSend && !audioRecv) { + // Audio disabled: have we removed it? + if(media.removeAudio && audioTransceiver) { + try { + if (audioTransceiver.setDirection) { + audioTransceiver.setDirection("inactive"); + } else { + audioTransceiver.direction = "inactive"; + } + Janus.log("Setting audio transceiver to inactive:", audioTransceiver); + } catch(e) { + Janus.error(e); + } + } + } else { + // Take care of audio m-line + if(audioSend && audioRecv) { + if(audioTransceiver) { + try { + if (audioTransceiver.setDirection) { + audioTransceiver.setDirection("sendrecv"); + } else { + audioTransceiver.direction = "sendrecv"; + } + Janus.log("Setting audio transceiver to sendrecv:", audioTransceiver); + } catch(e) { + Janus.error(e); + } + } + } else if(audioSend && !audioRecv) { + try { + if(audioTransceiver) { + if (audioTransceiver.setDirection) { + audioTransceiver.setDirection("sendonly"); + } else { + audioTransceiver.direction = "sendonly"; + } + Janus.log("Setting audio transceiver to sendonly:", audioTransceiver); + } + } catch(e) { + Janus.error(e); + } + } else if(!audioSend && audioRecv) { + if(audioTransceiver) { + try { + if (audioTransceiver.setDirection) { + audioTransceiver.setDirection("recvonly"); + } else { + audioTransceiver.direction = "recvonly"; + } + Janus.log("Setting audio transceiver to recvonly:", audioTransceiver); + } catch(e) { + Janus.error(e); + } + } else { + // In theory, this is the only case where we might not have a transceiver yet + audioTransceiver = config.pc.addTransceiver("audio", { direction: "recvonly" }); + Janus.log("Adding recvonly audio transceiver:", audioTransceiver); + } + } + } + // Handle video (and related changes, if any) + var videoSend = isVideoSendEnabled(media); + var videoRecv = isVideoRecvEnabled(media); + if(!videoSend && !videoRecv) { + // Video disabled: have we removed it? + if(media.removeVideo && videoTransceiver) { + try { + if (videoTransceiver.setDirection) { + videoTransceiver.setDirection("inactive"); + } else { + videoTransceiver.direction = "inactive"; + } + Janus.log("Setting video transceiver to inactive:", videoTransceiver); + } catch(e) { + Janus.error(e); + } + } + } else { + // Take care of video m-line + if(videoSend && videoRecv) { + if(videoTransceiver) { + try { + if (videoTransceiver.setDirection) { + videoTransceiver.setDirection("sendrecv"); + } else { + videoTransceiver.direction = "sendrecv"; + } + Janus.log("Setting video transceiver to sendrecv:", videoTransceiver); + } catch(e) { + Janus.error(e); + } + } + } else if(videoSend && !videoRecv) { + if(videoTransceiver) { + try { + if (videoTransceiver.setDirection) { + videoTransceiver.setDirection("sendonly"); + } else { + videoTransceiver.direction = "sendonly"; + } + Janus.log("Setting video transceiver to sendonly:", videoTransceiver); + } catch(e) { + Janus.error(e); + } + } + } else if(!videoSend && videoRecv) { + if(videoTransceiver) { + try { + if (videoTransceiver.setDirection) { + videoTransceiver.setDirection("recvonly"); + } else { + videoTransceiver.direction = "recvonly"; + } + Janus.log("Setting video transceiver to recvonly:", videoTransceiver); + } catch(e) { + Janus.error(e); + } + } else { + // In theory, this is the only case where we might not have a transceiver yet + videoTransceiver = config.pc.addTransceiver("video", { direction: "recvonly" }); + Janus.log("Adding recvonly video transceiver:", videoTransceiver); + } + } + } + } else { + if(Janus.webRTCAdapter.browserDetails.browser === "firefox" || Janus.webRTCAdapter.browserDetails.browser === "edge") { + mediaConstraints = { + offerToReceiveAudio: isAudioRecvEnabled(media), + offerToReceiveVideo: isVideoRecvEnabled(media) + }; + } else { + mediaConstraints = { + mandatory: { + OfferToReceiveAudio: isAudioRecvEnabled(media), + OfferToReceiveVideo: isVideoRecvEnabled(media) + } + }; + } + } + Janus.debug(mediaConstraints); + // Check if this is Firefox and we've been asked to do simulcasting + var sendVideo = isVideoSendEnabled(media); + if(sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === "firefox") { + // FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b + Janus.log("Enabling Simulcasting for Firefox (RID)"); + var sender = config.pc.getSenders()[1]; + Janus.log(sender); + var parameters = sender.getParameters(); + Janus.log(parameters); + + var maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates); + sender.setParameters({encodings: callbacks.sendEncodings || [ + { rid: "h", active: true, maxBitrate: maxBitrates.high }, + { rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2}, + { rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4} + ]}); + } + config.pc.createAnswer(mediaConstraints) + .then(function(answer) { + Janus.debug(answer); + // JSON.stringify doesn't work on some WebRTC objects anymore + // See https://code.google.com/p/chromium/issues/detail?id=467366 + var jsep = { + "type": answer.type, + "sdp": answer.sdp + }; + callbacks.customizeSdp(jsep); + answer.sdp = jsep.sdp; + Janus.log("Setting local description"); + if(sendVideo && simulcast && !Janus.unifiedPlan) { + // We only do simulcast via SDP munging on older versions of Chrome and Safari + if(Janus.webRTCAdapter.browserDetails.browser === "chrome") { + // FIXME Apparently trying to simulcast when answering breaks video in Chrome... + //~ Janus.log("Enabling Simulcasting for Chrome (SDP munging)"); + //~ answer.sdp = mungeSdpForSimulcasting(answer.sdp); + Janus.warn("simulcast=true, but this is an answer, and video breaks in Chrome if we enable it"); + } + } + config.mySdp = { + type: "answer", + sdp: answer.sdp + }; + config.pc.setLocalDescription(answer) + .catch(callbacks.error); + config.mediaConstraints = mediaConstraints; + if(!config.iceDone && !config.trickle) { + // Don't do anything until we have all candidates + Janus.log("Waiting for all candidates..."); + return; + } + // If transforms are present, notify Janus that the media is end-to-end encrypted + if(config.senderTransforms || config.receiverTransforms) { + answer["e2ee"] = true; + } + callbacks.success(answer); + }, callbacks.error); + } + + function sendSDP(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle, not sending anything"); + return; + } + var config = pluginHandle.webrtcStuff; + Janus.log("Sending offer/answer SDP..."); + if(!config.mySdp) { + Janus.warn("Local SDP instance is invalid, not sending anything..."); + return; + } + config.mySdp = { + "type": config.pc.localDescription.type, + "sdp": config.pc.localDescription.sdp + }; + if(config.trickle === false) + config.mySdp["trickle"] = false; + Janus.debug(callbacks); + config.sdpSent = true; + callbacks.success(config.mySdp); + } + + function getVolume(handleId, remote) { + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + return 0; + } + var stream = remote ? "remote" : "local"; + var config = pluginHandle.webrtcStuff; + if(!config.volume[stream]) + config.volume[stream] = { value: 0 }; + // Start getting the volume, if audioLevel in getStats is supported (apparently + // they're only available in Chrome/Safari right now: https://webrtc-stats.callstats.io/) + if(config.pc.getStats && (Janus.webRTCAdapter.browserDetails.browser === "chrome" || + Janus.webRTCAdapter.browserDetails.browser === "safari")) { + if(remote && !config.remoteStream) { + Janus.warn("Remote stream unavailable"); + return 0; + } else if(!remote && !config.myStream) { + Janus.warn("Local stream unavailable"); + return 0; + } + if(!config.volume[stream].timer) { + Janus.log("Starting " + stream + " volume monitor"); + config.volume[stream].timer = setInterval(function() { + config.pc.getStats() + .then(function(stats) { + stats.forEach(function (res) { + if(!res || res.kind !== "audio") + return; + if((remote && !res.remoteSource) || (!remote && res.type !== "media-source")) + return; + config.volume[stream].value = (res.audioLevel ? res.audioLevel : 0); + }); + }); + }, 200); + return 0; // We don't have a volume to return yet + } + return config.volume[stream].value; + } else { + // audioInputLevel and audioOutputLevel seem only available in Chrome? audioLevel + // seems to be available on Chrome and Firefox, but they don't seem to work + Janus.warn("Getting the " + stream + " volume unsupported by browser"); + return 0; + } + } + + function isMuted(handleId, video) { + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + return true; + } + var config = pluginHandle.webrtcStuff; + if(!config.pc) { + Janus.warn("Invalid PeerConnection"); + return true; + } + if(!config.myStream) { + Janus.warn("Invalid local MediaStream"); + return true; + } + if(video) { + // Check video track + if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { + Janus.warn("No video track"); + return true; + } + return !config.myStream.getVideoTracks()[0].enabled; + } else { + // Check audio track + if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { + Janus.warn("No audio track"); + return true; + } + return !config.myStream.getAudioTracks()[0].enabled; + } + } + + function mute(handleId, video, mute) { + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + return false; + } + var config = pluginHandle.webrtcStuff; + if(!config.pc) { + Janus.warn("Invalid PeerConnection"); + return false; + } + if(!config.myStream) { + Janus.warn("Invalid local MediaStream"); + return false; + } + if(video) { + // Mute/unmute video track + if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { + Janus.warn("No video track"); + return false; + } + config.myStream.getVideoTracks()[0].enabled = !mute; + return true; + } else { + // Mute/unmute audio track + if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { + Janus.warn("No audio track"); + return false; + } + config.myStream.getAudioTracks()[0].enabled = !mute; + return true; + } + } + + function getBitrate(handleId) { + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + return "Invalid handle"; + } + var config = pluginHandle.webrtcStuff; + if(!config.pc) + return "Invalid PeerConnection"; + // Start getting the bitrate, if getStats is supported + if(config.pc.getStats) { + if(!config.bitrate.timer) { + Janus.log("Starting bitrate timer (via getStats)"); + config.bitrate.timer = setInterval(function() { + config.pc.getStats() + .then(function(stats) { + stats.forEach(function (res) { + if(!res) + return; + var inStats = false; + // Check if these are statistics on incoming media + if((res.mediaType === "video" || res.id.toLowerCase().indexOf("video") > -1) && + res.type === "inbound-rtp" && res.id.indexOf("rtcp") < 0) { + // New stats + inStats = true; + } else if(res.type == 'ssrc' && res.bytesReceived && + (res.googCodecName === "VP8" || res.googCodecName === "")) { + // Older Chromer versions + inStats = true; + } + // Parse stats now + if(inStats) { + config.bitrate.bsnow = res.bytesReceived; + config.bitrate.tsnow = res.timestamp; + if(config.bitrate.bsbefore === null || config.bitrate.tsbefore === null) { + // Skip this round + config.bitrate.bsbefore = config.bitrate.bsnow; + config.bitrate.tsbefore = config.bitrate.tsnow; + } else { + // Calculate bitrate + var timePassed = config.bitrate.tsnow - config.bitrate.tsbefore; + if(Janus.webRTCAdapter.browserDetails.browser === "safari") + timePassed = timePassed/1000; // Apparently the timestamp is in microseconds, in Safari + var bitRate = Math.round((config.bitrate.bsnow - config.bitrate.bsbefore) * 8 / timePassed); + if(Janus.webRTCAdapter.browserDetails.browser === "safari") + bitRate = parseInt(bitRate/1000); + config.bitrate.value = bitRate + ' kbits/sec'; + //~ Janus.log("Estimated bitrate is " + config.bitrate.value); + config.bitrate.bsbefore = config.bitrate.bsnow; + config.bitrate.tsbefore = config.bitrate.tsnow; + } + } + }); + }); + }, 1000); + return "0 kbits/sec"; // We don't have a bitrate value yet + } + return config.bitrate.value; + } else { + Janus.warn("Getting the video bitrate unsupported by browser"); + return "Feature unsupported by browser"; + } + } + + function webrtcError(error) { + Janus.error("WebRTC error:", error); + } + + function cleanupWebrtc(handleId, hangupRequest) { + Janus.log("Cleaning WebRTC stuff"); + var pluginHandle = pluginHandles[handleId]; + if(!pluginHandle) { + // Nothing to clean + return; + } + var config = pluginHandle.webrtcStuff; + if(config) { + if(hangupRequest === true) { + // Send a hangup request (we don't really care about the response) + var request = { "janus": "hangup", "transaction": Janus.randomString(12) }; + if(pluginHandle.token) + request["token"] = pluginHandle.token; + if(apisecret) + request["apisecret"] = apisecret; + Janus.debug("Sending hangup request (handle=" + handleId + "):"); + Janus.debug(request); + if(websockets) { + request["session_id"] = sessionId; + request["handle_id"] = handleId; + ws.send(JSON.stringify(request)); + } else { + Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, { + verb: 'POST', + withCredentials: withCredentials, + body: request + }); + } + } + // Cleanup stack + config.remoteStream = null; + if(config.volume) { + if(config.volume["local"] && config.volume["local"].timer) + clearInterval(config.volume["local"].timer); + if(config.volume["remote"] && config.volume["remote"].timer) + clearInterval(config.volume["remote"].timer); + } + config.volume = {}; + if(config.bitrate.timer) + clearInterval(config.bitrate.timer); + config.bitrate.timer = null; + config.bitrate.bsnow = null; + config.bitrate.bsbefore = null; + config.bitrate.tsnow = null; + config.bitrate.tsbefore = null; + config.bitrate.value = null; + if(!config.streamExternal && config.myStream) { + Janus.log("Stopping local stream tracks"); + Janus.stopAllTracks(config.myStream); + } + config.streamExternal = false; + config.myStream = null; + // Close PeerConnection + try { + config.pc.close(); + } catch(e) { + // Do nothing + } + config.pc = null; + config.candidates = null; + config.mySdp = null; + config.remoteSdp = null; + config.iceDone = false; + config.dataChannel = {}; + config.dtmfSender = null; + config.senderTransforms = null; + config.receiverTransforms = null; + } + pluginHandle.oncleanup(); + } + + // Helper method to munge an SDP to enable simulcasting (Chrome only) + function mungeSdpForSimulcasting(sdp) { + // Let's munge the SDP to add the attributes for enabling simulcasting + // (based on https://gist.github.com/ggarber/a19b4c33510028b9c657) + var lines = sdp.split("\r\n"); + var video = false; + var ssrc = [ -1 ], ssrc_fid = [ -1 ]; + var cname = null, msid = null, mslabel = null, label = null; + var insertAt = -1; + for(let i=0; i -1) { + // We're done, let's add the new attributes here + insertAt = i; + break; + } + } + continue; + } + if(!video) + continue; + var sim = lines[i].match(/a=ssrc-group:SIM (\d+) (\d+) (\d+)/); + if(sim) { + Janus.warn("The SDP already contains a SIM attribute, munging will be skipped"); + return sdp; + } + var fid = lines[i].match(/a=ssrc-group:FID (\d+) (\d+)/); + if(fid) { + ssrc[0] = fid[1]; + ssrc_fid[0] = fid[2]; + lines.splice(i, 1); i--; + continue; + } + if(ssrc[0]) { + var match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)') + if(match) { + cname = match[1]; + } + match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)') + if(match) { + msid = match[1]; + } + match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)') + if(match) { + mslabel = match[1]; + } + match = lines[i].match('a=ssrc:' + ssrc[0] + ' label:(.+)') + if(match) { + label = match[1]; + } + if(lines[i].indexOf('a=ssrc:' + ssrc_fid[0]) === 0) { + lines.splice(i, 1); i--; + continue; + } + if(lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) { + lines.splice(i, 1); i--; + continue; + } + } + if(lines[i].length == 0) { + lines.splice(i, 1); i--; + continue; + } + } + if(ssrc[0] < 0) { + // Couldn't find a FID attribute, let's just take the first video SSRC we find + insertAt = -1; + video = false; + for(let i=0; i -1) { + // We're done, let's add the new attributes here + insertAt = i; + break; + } + } + continue; + } + if(!video) + continue; + if(ssrc[0] < 0) { + var value = lines[i].match(/a=ssrc:(\d+)/); + if(value) { + ssrc[0] = value[1]; + lines.splice(i, 1); i--; + continue; + } + } else { + let match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)') + if(match) { + cname = match[1]; + } + match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)') + if(match) { + msid = match[1]; + } + match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)') + if(match) { + mslabel = match[1]; + } + match = lines[i].match('a=ssrc:' + ssrc[0] + ' label:(.+)') + if(match) { + label = match[1]; + } + if(lines[i].indexOf('a=ssrc:' + ssrc_fid[0]) === 0) { + lines.splice(i, 1); i--; + continue; + } + if(lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) { + lines.splice(i, 1); i--; + continue; + } + } + if(lines[i].length === 0) { + lines.splice(i, 1); i--; + continue; + } + } + } + if(ssrc[0] < 0) { + // Still nothing, let's just return the SDP we were asked to munge + Janus.warn("Couldn't find the video SSRC, simulcasting NOT enabled"); + return sdp; + } + if(insertAt < 0) { + // Append at the end + insertAt = lines.length; + } + // Generate a couple of SSRCs (for retransmissions too) + // Note: should we check if there are conflicts, here? + ssrc[1] = Math.floor(Math.random()*0xFFFFFFFF); + ssrc[2] = Math.floor(Math.random()*0xFFFFFFFF); + ssrc_fid[1] = Math.floor(Math.random()*0xFFFFFFFF); + ssrc_fid[2] = Math.floor(Math.random()*0xFFFFFFFF); + // Add attributes to the SDP + for(var i=0; i # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -set -ex - -if [ "$(whoami)" != root ]; then - echo "Only root can do that" - exit 1 -fi - -if [ "$1" != --do-the-thing ]; then - echo "This script will make some firstboot magic. Don't run it manually." - exit 1 -fi - - -# ========== Preparing ========== - -if [ ! -f /boot/pikvm.txt ]; then - exit 0 -fi - -if systemctl is-enabled -q kvmd-oled; then - # Stop regular kvmd-oled service and show first time setup status in oled - systemctl stop kvmd-oled || true - kvmd-oled --interval=0 --text="On-boot setup...\nDO NOT INTERRUPT!\nPlease wait" || true - has_oled=1 -fi - -# shellcheck disable=SC1090 -source <(dos2unix < /boot/pikvm.txt) - -# shellcheck disable=SC1091 -source /usr/share/kvmd/platform || true - -rw - - -# ========== First boot and/or Avahi configuration ========== - -make_avahi_service() { - local _base - local _serial - local _platform - _base=$(tr -d '\0' < /proc/device-tree/model || echo "Unknown base") - _serial=$( (cat /proc/device-tree/serial-number || echo "0000000000000000") | tr -d '\0' | tr '[:lower:]' '[:upper:]') - _platform="$PIKVM_MODEL-$PIKVM_VIDEO-$PIKVM_BOARD" - mkdir -p /etc/avahi/services - cat < /etc/avahi/services/pikvm.service - - - - pikvm-$_serial.local - - _pikvm._tcp - 443 - path=/ - protocol=https - description=PiKVM Web Server - model=$PIKVM_MODEL - video=$PIKVM_VIDEO - board=$PIKVM_BOARD - base=$_base - serial=$_serial - platform=$_platform - - - _https._tcp - 443 - path=/ - protocol=https - description=PiKVM Web Server - model=$PIKVM_MODEL - video=$PIKVM_VIDEO - board=$PIKVM_BOARD - base=$_base - serial=$_serial - model=$_platform - - -end_of_file -} - -if [ -n "$FIRSTBOOT$FIRST_BOOT" ]; then - ( \ - (umount /etc/machine-id || true) \ - && echo -n > /etc/machine-id \ - && systemd-machine-id-setup \ - ) || true - - rm -f /etc/ssh/ssh_host_* - ssh-keygen -v -A - - rm -f /etc/kvmd/nginx/ssl/* - rm -f /etc/kvmd/vnc/ssl/* - kvmd-gencert --do-the-thing - kvmd-gencert --do-the-thing --vnc - - if grep -q 'X-kvmd\.otgmsd' /etc/fstab; then - part=$(grep 'X-kvmd\.otgmsd' /etc/fstab | awk '{print $1}') - # shellcheck disable=SC2206 - splitted=(${part//=/ }) - if [ "${splitted[0]}" == LABEL ]; then - label=${splitted[1]} - part=$(blkid -c /dev/null -L "$label") - else - label=PIMSD - fi - unset splitted - disk=/dev/$(lsblk -no pkname "$part") - npart=$(cat "/sys/class/block/${part//\/dev\//}/partition") - umount "$part" - parted "$disk" -a optimal -s resizepart "$npart" 100% - yes | mkfs.ext4 -L "$label" -F -m 0 "$part" - mount "$part" - unset disk part npart label - fi - - make_avahi_service - - # fc-cache is required for installed X server - # shellcheck disable=SC2015 - which fc-cache && fc-cache || true -fi - -if [ -n "$ENABLE_AVAHI" ]; then - if [ ! -f /etc/avahi/services/pikvm.service ]; then - make_avahi_service - fi - systemctl enable avahi-daemon || true - touch /boot/pikvm-reboot.txt -fi - - -# ========== OTG serial ========== - -if [ -n "$ENABLE_OTG_SERIAL" ]; then - cat < /etc/kvmd/override.d/0000-vendor-otg-serial.yaml -# Generated by kvmd-bootconfig. Do not edit this file! -otg: - devices: - serial: - enabled: true -end_of_file - grep '^ttyGS0$' /etc/securetty || echo ttyGS0 >> /etc/securetty - mkdir -p /etc/systemd/system/getty@ttyGS0.service.d - cat < /etc/systemd/system/getty@ttyGS0.service.d/override.conf -[Service] -TTYReset=no -TTYVHangup=no -TTYVTDisallocate=no -end_of_file - systemctl enable getty@ttyGS0.service - touch /boot/pikvm-reboot.txt -fi - - -# ========== SSH ========== - -if [ -n "$SSH_PORT" ]; then - sed -i -e "s/^\s*#*\s*Port\s\+.*$/Port $SSH_PORT/g" /etc/ssh/sshd_config -fi - - -# ========== Ethernet ========== - -make_dhcp_iface() { - local _iface="$1" - local _metric="$2" - cat < "/etc/systemd/network/$_iface.network" -[Match] -Name=$_iface - -[Network] -DHCP=yes -DNSSEC=no - -[DHCP] -# Use same IP by forcing to use MAC address for clientID -ClientIdentifier=mac -# https://github.com/pikvm/pikvm/issues/583 -RouteMetric=$_metric -end_of_file -} - -make_static_iface() { - local _iface="$1" - local _addr="$2" - local _gw="$3" - local _dns="$4" - local _metric="$5" - cat < "/etc/systemd/network/$_iface.network" -[Match] -Name=$_iface - -[Network] -Address=$_addr -DNS=$_dns -DNSSEC=no - -[Route] -Gateway=$_gw -# https://github.com/pikvm/pikvm/issues/583 -Metric=$_metric -end_of_file -} - -# If the ETH_DHCP is defined, configure eth0 for DHCP -if [ -n "$ETH_DHCP" ]; then - ETH_IFACE="${ETH_IFACE:-eth0}" - make_dhcp_iface "$ETH_IFACE" 10 -fi - -# If the ETH_ADDR is defined, configure a static address on eth0 -if [ -n "$ETH_ADDR" ]; then - ETH_IFACE="${ETH_IFACE:-eth0}" - make_static_iface "$ETH_IFACE" "$ETH_ADDR" "$ETH_GW" "$ETH_DNS" 10 -fi - - -# ========== Wi-Fi ========== - -# Set the regulatory domain for wifi, if defined. -if [ -n "$WIFI_REGDOM" ]; then - sed -i \ - -e 's/^\(WIRELESS_REGDOM=.*\)$/#\1/' \ - -e 's/^#\(WIRELESS_REGDOM="'"$WIFI_REGDOM"'"\)/\1/' \ - /etc/conf.d/wireless-regdom -fi - -# If the WIFI_ESSID is defined, configure wlan0 -if [ -n "$WIFI_ESSID" ]; then - WIFI_IFACE="${WIFI_IFACE:-wlan0}" - if [ -n "$WIFI_ADDR" ]; then - make_static_iface "$WIFI_IFACE" "$WIFI_ADDR" "$WIFI_GW" "$WIFI_DNS" 50 - else - make_dhcp_iface "$WIFI_IFACE" 50 - fi - wpa_passphrase "$WIFI_ESSID" "$WIFI_PASSWD" > "/etc/wpa_supplicant/wpa_supplicant-$WIFI_IFACE.conf" - chmod 640 "/etc/wpa_supplicant/wpa_supplicant-$WIFI_IFACE.conf" - if [ -n "$WIFI_HIDDEN" ]; then - sed -i -e 's/^}/\tscan_ssid=1\n}/g' "/etc/wpa_supplicant/wpa_supplicant-$WIFI_IFACE.conf" - fi - systemctl enable "wpa_supplicant@$WIFI_IFACE.service" || true - touch /boot/pikvm-reboot.txt -fi - - -# ========== Custom scripts ========== - -if [ -d /boot/pikvm-scripts.d ]; then - run-parts --regex='^.+$' /boot/pikvm-scripts.d || true -fi - - -# ========== Finish ========== -rm -f /boot/pikvm.txt - -if [ -f /boot/pikvm-reboot.txt ]; then - rm -f /boot/pikvm-reboot.txt - ro - echo "kvmd-bootconfig: Reboot after 5 seconds" | tee /dev/kmsg - sleep 2 - reboot - sleep 3 -else - ro - if [ -n "$has_oled" ]; then - # Critical tasks have completed so start kvmd-oled service as an indicator that on-boot tasks are complete - systemctl start kvmd-oled || true - fi -fi diff --git a/scripts/kvmd-certbot b/scripts/kvmd-certbot deleted file mode 100755 index ebc94e50..00000000 --- a/scripts/kvmd-certbot +++ /dev/null @@ -1,157 +0,0 @@ -#!/bin/bash -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -set -e -export LC_ALL=C - -if [ "$(whoami)" != root ]; then - echo "Only root can do that" - exit 1 -fi - -user=kvmd-certbot -web=/run/kvmd-certbot/webroot -cur=/var/lib/kvmd/pst/data/certbot/runroot -new=/var/lib/kvmd/pst/data/certbot/runroot.new -tmp=/tmp/kvmd-certbot/runroot - -cur_opts=("--config-dir=$cur/config" "--work-dir=$cur/work" "--logs-dir=$cur/logs") - - -function cleanup() { - rm -rf "$tmp" -} - -function create_tmp() { - mkdir "$tmp" # Acts as a lock - chown "$user:" "$tmp" - trap cleanup EXIT -} - -function ensure_runroot() { - kvmd-pstrun -- bash -c " - set -ex - mkdir -p '$cur' - chown -R '$user:' '$cur/..' - " -} - -function restart_if_running() { - if systemctl is-active --quiet "$2"; then - echo "=> systemctl $1 $2" - systemctl "$1" "$2" || true - fi -} - -function restart_if_running_nginx() { - restart_if_running reload kvmd-nginx -} - -function restart_if_running_cloud() { - restart_if_running reload kvmd-nginx -} - -function restart_if_running_vnc() { - restart_if_running restart kvmd-vnc -} - -case "$1" in - -h|--help|help) - create_tmp - sudo --preserve-env -u "$user" certbot "$@" "${cur_opts[@]}" - ;; - - --) - shift - create_tmp - ensure_runroot - sudo --preserve-env -u "$user" kvmd-pstrun -- certbot "$@" "${cur_opts[@]}" - ;; - - certonly|certonly_webroot) - webroot_opts=() - if [ "$1" == certonly_webroot ]; then - webroot_opts=(--webroot "--webroot-path=$web") - fi - shift - create_tmp - ensure_runroot - sudo --preserve-env -u "$user" kvmd-pstrun -- certbot certonly "$@" "${cur_opts[@]}" \ - "${webroot_opts[@]}" \ - --deploy-hook="/usr/bin/bash -c ' - set -ex - chmod 755 '$cur/config/'{archive,live} - chmod 640 \"\$RENEWED_LINEAGE/privkey.pem\" - '" - ;; - - renew) - shift - create_tmp - cp -a "$cur"/. "$tmp" - chown -R "$user:" "$tmp" - sed -s -i -e "s| = $cur/| = $tmp/|g" "$tmp/config/renewal/"* - sudo --preserve-env -u "$user" certbot renew "$@" \ - --config-dir="$tmp/config" \ - --work-dir="$tmp/work" \ - --logs-dir="$tmp/logs" \ - --deploy-hook="/usr/bin/touch '$tmp/updated'" || true - if [ -f "$tmp/updated" ]; then - sudo --preserve-env -u "$user" kvmd-pstrun -- bash -c " - set -ex - rm -rf '$new' - cp -a '$tmp'/. '$new' - rm '$new/updated' - chmod 755 '$new/config/'{archive,live} - chmod 640 '$new'/config/archive/*/privkey*.pem - sed -s -i -e 's| = $tmp/| = $cur/|g' '$new/config/renewal/'* - rm -rf '$new/logs/'*.log.* '$new/config/'{csr,keys} - sync - kvmd-helper-swapfiles '$new' '$cur' - rm -rf '$new' - " - restart_if_running_nginx - restart_if_running_vnc - fi - ;; - - install_nginx|install_vnc|install_cloud) - target="${1/install_/}" - if [ -z "$2" ]; then - echo "Usage: kvmd-certbot $1 " - exit 1 - fi - set -x - rm -f "/etc/kvmd/$target/ssl/server."{crt,key} - ln -s "$cur/config/live/$2/fullchain.pem" "/etc/kvmd/$target/ssl/server.crt" - ln -s "$cur/config/live/$2/privkey.pem" "/etc/kvmd/$target/ssl/server.key" - "restart_if_running_$target" - ;; - - *) - echo "This command is not implemented by kvmd-certbot." - echo "To pass it into certbot under PST context use '--'." - echo "For example: kvmd-certbot -- $*" - exit 1 - ;; -esac diff --git a/scripts/kvmd-htpasswd b/scripts/kvmd-htpasswd deleted file mode 100755 index ef94bf4b..00000000 --- a/scripts/kvmd-htpasswd +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -cd / && python3 -m kvmd.apps.htpasswd "$@" \ No newline at end of file diff --git a/scripts/kvmd-otg b/scripts/kvmd-otg deleted file mode 100755 index 6088326f..00000000 --- a/scripts/kvmd-otg +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -cd / && python3 -m kvmd.apps.otg "$@" \ No newline at end of file diff --git a/scripts/kvmd-otgconf b/scripts/kvmd-otgconf deleted file mode 100755 index d307fbf5..00000000 --- a/scripts/kvmd-otgconf +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -cd / && python3 -m kvmd.apps.otgconf "$@" \ No newline at end of file diff --git a/scripts/kvmd-totp b/scripts/kvmd-totp deleted file mode 100755 index 9e48110a..00000000 --- a/scripts/kvmd-totp +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -cd / && python3 -m kvmd.apps.totp "$@" \ No newline at end of file diff --git a/scripts/kvmd-udev-hdmiusb-check b/scripts/kvmd-udev-hdmiusb-check deleted file mode 100755 index 12c19204..00000000 --- a/scripts/kvmd-udev-hdmiusb-check +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -set -x - -# udevadm info -a -p $(udevadm info -q path -n /dev/video0) - -board="$1" -port="$2" - -# https://gist.github.com/mdevaev/6eb65be689142e5ac16da7542f50830f -model="$(tr < /proc/device-tree/model -d '\000')" - -case "$board" in - "rpi2") - case "$model" in - "Raspberry Pi 2 Model B Rev 1.1") - if [ "$port" == "1-1.5:1.0" ]; then exit 0; else exit 1; fi;; - *) exit 0;; - esac;; - "rpi3") - case "$model" in - "Raspberry Pi 3 Model B Rev 1.2") - if [ "$port" == "1-1.5:1.0" ]; then exit 0; else exit 1; fi;; - "Raspberry Pi 3 Model B Plus Rev 1.3") - if [ "$port" == "1-1.2:1.0" ]; then exit 0; else exit 1; fi;; - *) exit 0;; - esac;; - "rpi4") - case "$model" in - "Raspberry Pi 4 Model B Rev 1.1" | "Raspberry Pi 4 Model B Rev 1.2" | "Raspberry Pi 4 Model B Rev 1.4" | "Raspberry Pi 4 Model B Rev 1.5") - if [ "$port" == "1-1.4:1.0" ] || [ "$port" == "2-1:1.0" ]; then exit 0; else exit 1; fi;; - *) exit 0;; - esac;; - *) exit 1;; -esac diff --git a/scripts/kvmd-udev-restart-pass b/scripts/kvmd-udev-restart-pass deleted file mode 100755 index 4b77897a..00000000 --- a/scripts/kvmd-udev-restart-pass +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2024 Maxim Devaev # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ========================================================================== # - - -set -eE -set -x - -# https://forums.raspberrypi.com/viewtopic.php?p=2202017 - -card="$1" -conn_id="$2" - -test -n "$card" -test -n "$conn_id" - -card_n="${card/card/}" -test -n "$card_n" - -port=$(grep "^connector\[$conn_id\]:" "/sys/kernel/debug/dri/$card_n/state" | awk '{print $2}') -test -n "$port" - - -if [ "$port" = "HDMI-A-1" ]; then - status=$(head -n 1 "/sys/class/drm/$card-$port/status") - if [ "$status" = "connected" ]; then - for pid in $(pgrep -f '^kvmd/streamer: ' || true); do - kill "$pid" || true - done - fi -fi diff --git a/scripts/kvmd-vnc b/scripts/kvmd-vnc deleted file mode 100755 index cef9b59f..00000000 --- a/scripts/kvmd-vnc +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -cd / && python3 -m kvmd.apps.vnc "$@" \ No newline at end of file diff --git a/web/extras/.gitignore b/web/extras/.gitignore deleted file mode 100644 index e69de29b..00000000