move shared code to pkg.

This commit is contained in:
Miroslav Šedivý
2022-03-20 11:43:00 +01:00
parent 94c17e9a42
commit 8593d2d0fd
93 changed files with 132 additions and 133 deletions

6
pkg/types/api.go Normal file
View File

@ -0,0 +1,6 @@
package types
type ApiManager interface {
Route(r Router)
AddRouter(path string, router func(Router))
}

163
pkg/types/capture.go Normal file
View File

@ -0,0 +1,163 @@
package types
import (
"context"
"errors"
"fmt"
"math"
"strings"
"github.com/PaesslerAG/gval"
"github.com/pion/webrtc/v3/pkg/media"
"gitlab.com/demodesk/neko/server/pkg/types/codec"
)
var (
ErrCapturePipelineAlreadyExists = errors.New("capture pipeline already exists")
)
type Sample media.Sample
type BroadcastManager interface {
Start(url string) error
Stop()
Started() bool
Url() string
}
type ScreencastManager interface {
Enabled() bool
Started() bool
Image() ([]byte, error)
}
type StreamSinkManager interface {
Codec() codec.RTPCodec
AddListener(listener *func(sample Sample)) error
RemoveListener(listener *func(sample Sample)) error
MoveListenerTo(listener *func(sample Sample), targetStream StreamSinkManager) error
ListenersCount() int
Started() bool
}
type StreamSrcManager interface {
Codec() codec.RTPCodec
Start(codec codec.RTPCodec) error
Stop()
Push(bytes []byte)
Started() bool
}
type CaptureManager interface {
Start()
Shutdown() error
Broadcast() BroadcastManager
Screencast() ScreencastManager
Audio() StreamSinkManager
Video(videoID string) (StreamSinkManager, bool)
VideoIDs() []string
Webcam() StreamSrcManager
Microphone() StreamSrcManager
}
type VideoConfig struct {
Width string `mapstructure:"width"` // expression
Height string `mapstructure:"height"` // expression
Fps string `mapstructure:"fps"` // expression
GstPrefix string `mapstructure:"gst_prefix"` // pipeline prefix, starts with !
GstEncoder string `mapstructure:"gst_encoder"` // gst encoder name
GstParams map[string]string `mapstructure:"gst_params"` // map of expressions
GstSuffix string `mapstructure:"gst_suffix"` // pipeline suffix, starts with !
GstPipeline string `mapstructure:"gst_pipeline"` // whole pipeline as a string
}
func (config *VideoConfig) GetPipeline(screen ScreenSize) (string, error) {
values := map[string]interface{}{
"width": screen.Width,
"height": screen.Height,
"fps": screen.Rate,
}
language := []gval.Language{
gval.Function("round", func(args ...interface{}) (interface{}, error) {
return (int)(math.Round(args[0].(float64))), nil
}),
}
// get fps pipeline
fpsPipeline := "! video/x-raw ! videoconvert ! queue"
if config.Fps != "" {
eval, err := gval.Full(language...).NewEvaluable(config.Fps)
if err != nil {
return "", err
}
val, err := eval.EvalFloat64(context.Background(), values)
if err != nil {
return "", err
}
fpsPipeline = fmt.Sprintf("! video/x-raw,framerate=%d/100 ! videoconvert ! queue", int(val*100))
}
// get scale pipeline
scalePipeline := ""
if config.Width != "" && config.Height != "" {
eval, err := gval.Full(language...).NewEvaluable(config.Width)
if err != nil {
return "", err
}
w, err := eval.EvalInt(context.Background(), values)
if err != nil {
return "", err
}
eval, err = gval.Full(language...).NewEvaluable(config.Height)
if err != nil {
return "", err
}
h, err := eval.EvalInt(context.Background(), values)
if err != nil {
return "", err
}
scalePipeline = fmt.Sprintf("! videoscale ! video/x-raw,width=%d,height=%d ! queue", w, h)
}
// get encoder pipeline
encPipeline := fmt.Sprintf("! %s", config.GstEncoder)
for key, expr := range config.GstParams {
if expr == "" {
continue
}
val, err := gval.Evaluate(expr, values, language...)
if err != nil {
return "", err
}
if val != nil {
encPipeline += fmt.Sprintf(" %s=%v", key, val)
} else {
encPipeline += fmt.Sprintf(" %s=%s", key, expr)
}
}
// join strings with space
return strings.Join([]string{
fpsPipeline,
scalePipeline,
config.GstPrefix,
encPipeline,
config.GstSuffix,
}[:], " "), nil
}

183
pkg/types/codec/codecs.go Normal file
View File

@ -0,0 +1,183 @@
package codec
import (
"strings"
"github.com/pion/webrtc/v3"
)
func ParseRTC(codec webrtc.RTPCodecParameters) (RTPCodec, bool) {
codecName := strings.Split(codec.RTPCodecCapability.MimeType, "/")[1]
return ParseStr(codecName)
}
func ParseStr(codecName string) (codec RTPCodec, ok bool) {
ok = true
switch strings.ToLower(codecName) {
case VP8().Name:
codec = VP8()
case VP9().Name:
codec = VP9()
case H264().Name:
codec = H264()
case Opus().Name:
codec = Opus()
case G722().Name:
codec = G722()
case PCMU().Name:
codec = PCMU()
case PCMA().Name:
codec = PCMA()
default:
ok = false
}
return
}
type RTPCodec struct {
Name string
PayloadType webrtc.PayloadType
Type webrtc.RTPCodecType
Capability webrtc.RTPCodecCapability
Pipeline string
}
func (codec *RTPCodec) Register(engine *webrtc.MediaEngine) error {
return engine.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: codec.Capability,
PayloadType: codec.PayloadType,
}, codec.Type)
}
func VP8() RTPCodec {
return RTPCodec{
Name: "vp8",
PayloadType: 96,
Type: webrtc.RTPCodecTypeVideo,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/vpx/vp8enc.html
// gstreamer1.0-plugins-good
Pipeline: "vp8enc cpu-used=16 threads=4 deadline=1 error-resilient=partitions keyframe-max-dist=15 static-threshold=20",
}
}
// TODO: Profile ID.
func VP9() RTPCodec {
return RTPCodec{
Name: "vp9",
PayloadType: 98,
Type: webrtc.RTPCodecTypeVideo,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "profile-id=0",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/vpx/vp9enc.html
// gstreamer1.0-plugins-good
Pipeline: "vp9enc cpu-used=16 threads=4 deadline=1 keyframe-max-dist=15 static-threshold=20",
}
}
// TODO: Profile ID.
func H264() RTPCodec {
return RTPCodec{
Name: "h264",
PayloadType: 102,
Type: webrtc.RTPCodecTypeVideo,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/x264/index.html
// gstreamer1.0-plugins-ugly
Pipeline: "video/x-raw,format=I420 ! x264enc threads=4 bitrate=4096 key-int-max=15 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream",
// https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html
// gstreamer1.0-plugins-bad
//Pipeline: "openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000 ! video/x-h264,stream-format=byte-stream",
}
}
func Opus() RTPCodec {
return RTPCodec{
Name: "opus",
PayloadType: 111,
Type: webrtc.RTPCodecTypeAudio,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/opus/opusenc.html
// gstreamer1.0-plugins-base
Pipeline: "opusenc bitrate=128000",
}
}
func G722() RTPCodec {
return RTPCodec{
Name: "g722",
PayloadType: 9,
Type: webrtc.RTPCodecTypeAudio,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeG722,
ClockRate: 8000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/libav/avenc_g722.html
// gstreamer1.0-libav
Pipeline: "avenc_g722",
}
}
func PCMU() RTPCodec {
return RTPCodec{
Name: "pcmu",
PayloadType: 0,
Type: webrtc.RTPCodecTypeAudio,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMU,
ClockRate: 8000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/mulaw/mulawenc.html
// gstreamer1.0-plugins-good
Pipeline: "audio/x-raw, rate=8000 ! mulawenc",
}
}
func PCMA() RTPCodec {
return RTPCodec{
Name: "pcma",
PayloadType: 8,
Type: webrtc.RTPCodecTypeAudio,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMA,
ClockRate: 8000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/alaw/alawenc.html
// gstreamer1.0-plugins-good
Pipeline: "audio/x-raw, rate=8000 ! alawenc",
}
}

90
pkg/types/desktop.go Normal file
View File

@ -0,0 +1,90 @@
package types
import (
"image"
)
type CursorImage struct {
Width uint16
Height uint16
Xhot uint16
Yhot uint16
Serial uint64
Image *image.RGBA
}
type ScreenSize struct {
Width int
Height int
Rate int16
}
type ScreenConfiguration struct {
Width int
Height int
Rates map[int]int16
}
type KeyboardModifiers struct {
NumLock *bool
CapsLock *bool
}
type KeyboardMap struct {
Layout string
Variant string
}
type ClipboardText struct {
Text string
HTML string
}
type DesktopManager interface {
Start()
Shutdown() error
OnBeforeScreenSizeChange(listener func())
OnAfterScreenSizeChange(listener func())
// xorg
Move(x, y int)
GetCursorPosition() (int, int)
Scroll(x, y int)
ButtonDown(code uint32) error
KeyDown(code uint32) error
ButtonUp(code uint32) error
KeyUp(code uint32) error
KeyPress(codes ...uint32) error
ResetKeys()
ScreenConfigurations() map[int]ScreenConfiguration
SetScreenSize(ScreenSize) error
GetScreenSize() *ScreenSize
SetKeyboardMap(KeyboardMap) error
GetKeyboardMap() (*KeyboardMap, error)
SetKeyboardModifiers(mod KeyboardModifiers)
GetKeyboardModifiers() KeyboardModifiers
GetCursorImage() *CursorImage
GetScreenshotImage() *image.RGBA
// xevent
OnCursorChanged(listener func(serial uint64))
OnClipboardUpdated(listener func())
OnFileChooserDialogOpened(listener func())
OnFileChooserDialogClosed(listener func())
OnEventError(listener func(error_code uint8, message string, request_code uint8, minor_code uint8))
// clipboard
ClipboardGetText() (*ClipboardText, error)
ClipboardSetText(data ClipboardText) error
ClipboardGetBinary(mime string) ([]byte, error)
ClipboardSetBinary(mime string, data []byte) error
ClipboardGetTargets() ([]string, error)
// drop
DropFiles(x int, y int, files []string) bool
// filechooser
HandleFileChooserDialog(uri string) error
CloseFileChooserDialog()
IsFileChooserDialogOpened() bool
}

74
pkg/types/event/events.go Normal file
View File

@ -0,0 +1,74 @@
package event
const (
SYSTEM_INIT = "system/init"
SYSTEM_ADMIN = "system/admin"
SYSTEM_LOGS = "system/logs"
SYSTEM_DISCONNECT = "system/disconnect"
)
const (
SIGNAL_REQUEST = "signal/request"
SIGNAL_RESTART = "signal/restart"
SIGNAL_OFFER = "signal/offer"
SIGNAL_ANSWER = "signal/answer"
SIGNAL_PROVIDE = "signal/provide"
SIGNAL_CANDIDATE = "signal/candidate"
SIGNAL_VIDEO = "signal/video"
SIGNAL_CLOSE = "signal/close"
)
const (
SESSION_CREATED = "session/created"
SESSION_DELETED = "session/deleted"
SESSION_PROFILE = "session/profile"
SESSION_STATE = "session/state"
SESSION_CURSORS = "session/cursors"
)
const (
CONTROL_HOST = "control/host"
CONTROL_RELEASE = "control/release"
CONTROL_REQUEST = "control/request"
// mouse
CONTROL_MOVE = "control/move" // TODO: New. (fallback)
CONTROL_SCROLL = "control/scroll" // TODO: New. (fallback)
// keyboard
CONTROL_KEYPRESS = "control/keypress"
CONTROL_KEYDOWN = "control/keydown"
CONTROL_KEYUP = "control/keyup"
// actions
CONTROL_CUT = "control/cut"
CONTROL_COPY = "control/copy"
CONTROL_PASTE = "control/paste"
CONTROL_SELECT_ALL = "control/select_all"
)
const (
SCREEN_UPDATED = "screen/updated"
SCREEN_SET = "screen/set"
)
const (
CLIPBOARD_UPDATED = "clipboard/updated"
CLIPBOARD_SET = "clipboard/set"
)
const (
KEYBOARD_MODIFIERS = "keyboard/modifiers"
KEYBOARD_MAP = "keyboard/map"
)
const (
BORADCAST_STATUS = "broadcast/status"
)
const (
SEND_UNICAST = "send/unicast"
SEND_BROADCAST = "send/broadcast"
)
const (
FILE_CHOOSER_DIALOG_OPENED = "file_chooser_dialog/opened"
FILE_CHOOSER_DIALOG_CLOSED = "file_chooser_dialog/closed"
)

28
pkg/types/http.go Normal file
View File

@ -0,0 +1,28 @@
package types
import (
"context"
"net/http"
)
type RouterHandler func(w http.ResponseWriter, r *http.Request) error
type MiddlewareHandler func(w http.ResponseWriter, r *http.Request) (context.Context, error)
type Router interface {
Group(fn func(Router))
Route(pattern string, fn func(Router))
Get(pattern string, fn RouterHandler)
Post(pattern string, fn RouterHandler)
Put(pattern string, fn RouterHandler)
Delete(pattern string, fn RouterHandler)
With(fn MiddlewareHandler) Router
WithBypass(fn func(next http.Handler) http.Handler) Router
Use(fn MiddlewareHandler)
UseBypass(fn func(next http.Handler) http.Handler)
ServeHTTP(w http.ResponseWriter, req *http.Request)
}
type HttpManager interface {
Start()
Shutdown() error
}

43
pkg/types/member.go Normal file
View File

@ -0,0 +1,43 @@
package types
import "errors"
var (
ErrMemberAlreadyExists = errors.New("member already exists")
ErrMemberDoesNotExist = errors.New("member does not exist")
ErrMemberInvalidPassword = errors.New("invalid password")
)
type MemberProfile struct {
Name string `json:"name"`
IsAdmin bool `json:"is_admin"`
CanLogin bool `json:"can_login"`
CanConnect bool `json:"can_connect"`
CanWatch bool `json:"can_watch"`
CanHost bool `json:"can_host"`
CanShareMedia bool `json:"can_share_media"`
CanAccessClipboard bool `json:"can_access_clipboard"`
SendsInactiveCursor bool `json:"sends_inactive_cursor"`
CanSeeInactiveCursors bool `json:"can_see_inactive_cursors"`
}
type MemberProvider interface {
Connect() error
Disconnect() error
Authenticate(username string, password string) (string, MemberProfile, error)
Insert(username string, password string, profile MemberProfile) (string, error)
Select(id string) (MemberProfile, error)
SelectAll(limit int, offset int) (map[string]MemberProfile, error)
UpdateProfile(id string, profile MemberProfile) error
UpdatePassword(id string, password string) error
Delete(id string) error
}
type MemberManager interface {
MemberProvider
Login(username string, password string) (Session, string, error)
Logout(id string) error
}

View File

@ -0,0 +1,177 @@
package message
import (
"github.com/pion/webrtc/v3"
"gitlab.com/demodesk/neko/server/pkg/types"
)
/////////////////////////////
// System
/////////////////////////////
type SystemWebRTC struct {
Videos []string `json:"videos"`
}
type SystemInit struct {
SessionId string `json:"session_id"`
ControlHost ControlHost `json:"control_host"`
ScreenSize ScreenSize `json:"screen_size"`
Sessions map[string]SessionData `json:"sessions"`
ImplicitHosting bool `json:"implicit_hosting"`
InactiveCursors bool `json:"inactive_cursors"`
ScreencastEnabled bool `json:"screencast_enabled"`
WebRTC SystemWebRTC `json:"webrtc"`
}
type SystemAdmin struct {
ScreenSizesList []ScreenSize `json:"screen_sizes_list"`
BroadcastStatus BroadcastStatus `json:"broadcast_status"`
}
type SystemLogs = []SystemLog
type SystemLog struct {
Level string `json:"level"`
Fields map[string]interface{} `json:"fields"`
Message string `json:"message"`
}
type SystemDisconnect struct {
Message string `json:"message"`
}
/////////////////////////////
// Signal
/////////////////////////////
type SignalProvide struct {
SDP string `json:"sdp"`
ICEServers []types.ICEServer `json:"iceservers"`
Video string `json:"video"`
}
type SignalCandidate struct {
webrtc.ICECandidateInit
}
type SignalDescription struct {
SDP string `json:"sdp"`
}
type SignalVideo struct {
Video string `json:"video"`
}
/////////////////////////////
// Session
/////////////////////////////
type SessionID struct {
ID string `json:"id"`
}
type MemberProfile struct {
ID string `json:"id"`
types.MemberProfile
}
type SessionState struct {
ID string `json:"id"`
types.SessionState
}
type SessionData struct {
ID string `json:"id"`
Profile types.MemberProfile `json:"profile"`
State types.SessionState `json:"state"`
}
type SessionCursors struct {
ID string `json:"id"`
Cursors []types.Cursor `json:"cursors"`
}
/////////////////////////////
// Control
/////////////////////////////
type ControlHost struct {
HasHost bool `json:"has_host"`
HostID string `json:"host_id,omitempty"`
}
// TODO: New.
type ControlMove struct {
X uint16 `json:"x"`
Y uint16 `json:"y"`
}
// TODO: New.
type ControlScroll struct {
X int16 `json:"x"`
Y int16 `json:"y"`
}
type ControlKey struct {
Keysym uint32 `json:"keysym"`
}
/////////////////////////////
// Screen
/////////////////////////////
type ScreenSize struct {
Width int `json:"width"`
Height int `json:"height"`
Rate int16 `json:"rate"`
}
/////////////////////////////
// Clipboard
/////////////////////////////
type ClipboardData struct {
Text string `json:"text"`
}
/////////////////////////////
// Keyboard
/////////////////////////////
type KeyboardMap struct {
Layout string `json:"layout"`
Variant string `json:"variant"`
}
type KeyboardModifiers struct {
CapsLock *bool `json:"capslock"`
NumLock *bool `json:"numlock"`
}
/////////////////////////////
// Broadcast
/////////////////////////////
type BroadcastStatus struct {
IsActive bool `json:"is_active"`
URL string `json:"url,omitempty"`
}
/////////////////////////////
// Send (opaque comunication channel)
/////////////////////////////
type SendUnicast struct {
Sender string `json:"sender"`
Receiver string `json:"receiver"`
Subject string `json:"subject"`
Body interface{} `json:"body"`
}
type SendBroadcast struct {
Sender string `json:"sender"`
Subject string `json:"subject"`
Body interface{} `json:"body"`
}

81
pkg/types/session.go Normal file
View File

@ -0,0 +1,81 @@
package types
import (
"errors"
"net/http"
)
var (
ErrSessionNotFound = errors.New("session not found")
ErrSessionAlreadyExists = errors.New("session already exists")
ErrSessionAlreadyConnected = errors.New("session is already connected")
ErrSessionLoginDisabled = errors.New("session login disabled")
)
type Cursor struct {
X int `json:"x"`
Y int `json:"y"`
}
type SessionState struct {
IsConnected bool `json:"is_connected"`
IsWatching bool `json:"is_watching"`
}
type Session interface {
ID() string
Profile() MemberProfile
State() SessionState
IsHost() bool
// cursor
SetCursor(cursor Cursor)
// websocket
SetWebSocketPeer(websocketPeer WebSocketPeer)
SetWebSocketConnected(websocketPeer WebSocketPeer, connected bool)
GetWebSocketPeer() WebSocketPeer
Send(event string, payload interface{})
// webrtc
SetWebRTCPeer(webrtcPeer WebRTCPeer)
SetWebRTCConnected(webrtcPeer WebRTCPeer, connected bool)
GetWebRTCPeer() WebRTCPeer
}
type SessionManager interface {
Create(id string, profile MemberProfile) (Session, string, error)
Update(id string, profile MemberProfile) error
Delete(id string) error
Get(id string) (Session, bool)
GetByToken(token string) (Session, bool)
List() []Session
SetHost(host Session)
GetHost() Session
ClearHost()
SetCursor(cursor Cursor, session Session)
PopCursors() map[Session][]Cursor
Broadcast(event string, payload interface{}, exclude interface{})
AdminBroadcast(event string, payload interface{}, exclude interface{})
InactiveCursorsBroadcast(event string, payload interface{}, exclude interface{})
OnCreated(listener func(session Session))
OnDeleted(listener func(session Session))
OnConnected(listener func(session Session))
OnDisconnected(listener func(session Session))
OnProfileChanged(listener func(session Session))
OnStateChanged(listener func(session Session))
OnHostChanged(listener func(session Session))
ImplicitHosting() bool
InactiveCursors() bool
CookieEnabled() bool
MercifulReconnect() bool
CookieSetToken(w http.ResponseWriter, token string)
CookieClearToken(w http.ResponseWriter, r *http.Request)
Authenticate(r *http.Request) (Session, error)
}

42
pkg/types/webrtc.go Normal file
View File

@ -0,0 +1,42 @@
package types
import (
"errors"
"github.com/pion/webrtc/v3"
)
var (
ErrWebRTCVideoNotFound = errors.New("webrtc video not found")
ErrWebRTCDataChannelNotFound = errors.New("webrtc data channel not found")
ErrWebRTCConnectionNotFound = errors.New("webrtc connection not found")
)
type ICEServer struct {
URLs []string `mapstructure:"urls" json:"urls"`
Username string `mapstructure:"username" json:"username,omitempty"`
Credential string `mapstructure:"credential" json:"credential,omitempty"`
}
type WebRTCPeer interface {
CreateOffer(ICERestart bool) (*webrtc.SessionDescription, error)
CreateAnswer() (*webrtc.SessionDescription, error)
SetOffer(sdp string) error
SetAnswer(sdp string) error
SetCandidate(candidate webrtc.ICECandidateInit) error
SetVideoID(videoID string) error
SendCursorPosition(x, y int) error
SendCursorImage(cur *CursorImage, img []byte) error
Destroy()
}
type WebRTCManager interface {
Start()
Shutdown() error
ICEServers() []ICEServer
CreatePeer(session Session, videoID string) (*webrtc.SessionDescription, error)
}

28
pkg/types/websocket.go Normal file
View File

@ -0,0 +1,28 @@
package types
import (
"encoding/json"
"net/http"
)
type WebSocketMessage struct {
Event string `json:"event"`
Payload json.RawMessage `json:"payload"`
}
type WebSocketHandler func(Session, WebSocketMessage) bool
type CheckOrigin func(r *http.Request) bool
type WebSocketPeer interface {
Send(event string, payload interface{})
Ping() error
Destroy(reason string)
}
type WebSocketManager interface {
Start()
Shutdown() error
AddHandler(handler WebSocketHandler)
Upgrade(checkOrigin CheckOrigin) RouterHandler
}