diff --git a/.m1k1o/base/Dockerfile b/.m1k1o/base/Dockerfile index 8db79471..7371cb5f 100644 --- a/.m1k1o/base/Dockerfile +++ b/.m1k1o/base/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /src # install dependencies RUN set -eux; apt-get update; \ apt-get install -y --no-install-recommends git cmake make libx11-dev libxrandr-dev libxtst-dev \ - libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good; \ + libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly; \ # # install libclipboard set -eux; \ @@ -69,7 +69,7 @@ RUN set -eux; apt-get update; \ # # gst apt-get install -y --no-install-recommends libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ - gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-pulseaudio; \ + gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio; \ # # create a non-root user groupadd --gid $USER_GID $USERNAME; \ diff --git a/client/package.json b/client/package.json index 6497810a..1bd21792 100644 --- a/client/package.json +++ b/client/package.json @@ -19,49 +19,49 @@ "lint": "vue-cli-service lint" }, "dependencies": { - "@fortawesome/fontawesome-free": "^5.13.0", - "animejs": "^3.1.0", + "@fortawesome/fontawesome-free": "^5.14.0", + "animejs": "^3.2.0", "axios": "^0.19.1", - "date-fns": "^2.11.1", + "date-fns": "^2.16.1", "emoji-datasource": "^5.0.1", "emojilib": "^2.4.0", - "eventemitter3": "^4.0.0", + "eventemitter3": "^4.0.7", "resize-observer-polyfill": "^1.5.1", "simple-markdown": "^0.7.2", - "sweetalert2": "^9.10.9", - "typed-vuex": "^0.1.17", + "sweetalert2": "^9.17.2", + "typed-vuex": "^0.1.21", "v-tooltip": "^2.0.3", - "vue": "^2.6.10", - "vue-class-component": "^7.2.3", + "vue": "^2.6.12", + "vue-class-component": "^7.2.6", "vue-clickaway": "^2.2.2", - "vue-context": "^5.1.0", - "vue-i18n": "^8.16.0", + "vue-context": "^5.2.0", + "vue-i18n": "^8.21.1", "vue-notification": "^1.3.20", - "vue-property-decorator": "^8.4.1", - "vuex": "^3.1.3" + "vue-property-decorator": "^8.5.1", + "vuex": "^3.5.1" }, "devDependencies": { - "@types/animejs": "^3.1.0", - "@types/node": "^13.7.0", + "@types/animejs": "^3.1.2", + "@types/node": "^13.13.21", "@types/vue": "^2.0.0", "@types/vue-clickaway": "^2.2.0", - "@typescript-eslint/eslint-plugin": "^2.26.0", - "@typescript-eslint/parser": "^2.26.0", - "@vue/cli-plugin-babel": "^4.1.0", - "@vue/cli-plugin-eslint": "^4.1.0", - "@vue/cli-plugin-typescript": "^4.1.0", - "@vue/cli-plugin-vuex": "^4.1.0", - "@vue/cli-service": "^4.1.0", + "@typescript-eslint/eslint-plugin": "^2.34.0", + "@typescript-eslint/parser": "^2.34.0", + "@vue/cli-plugin-babel": "^4.5.6", + "@vue/cli-plugin-eslint": "^4.5.6", + "@vue/cli-plugin-typescript": "^4.5.6", + "@vue/cli-plugin-vuex": "^4.5.6", + "@vue/cli-service": "^4.5.6", "@vue/eslint-config-prettier": "^6.0.0", - "@vue/eslint-config-typescript": "^5.0.2", + "@vue/eslint-config-typescript": "^5.1.0", "eslint": "^6.8.0", - "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-vue": "^6.2.2", - "node-sass": "^4.12.0", - "prettier": "^2.0.2", + "node-sass": "^4.14.1", + "prettier": "^2.1.2", "sass-loader": "^8.0.0", - "ts-node": "^8.6.2", - "typescript": "^3.8.3", - "vue-template-compiler": "^2.6.10" + "ts-node": "^8.10.2", + "typescript": "^3.9.7", + "vue-template-compiler": "^2.6.12" } } diff --git a/client/src/components/settings.vue b/client/src/components/settings.vue index b5ae941d..612ee183 100644 --- a/client/src/components/settings.vue +++ b/client/src/components/settings.vue @@ -48,6 +48,19 @@ +
  • @@ -220,6 +233,30 @@ } } } + + .input { + display: block; + height: 30px; + text-align: right; + padding: 0 10px; + margin-left: 10px; + line-height: 30px; + text-overflow: ellipsis; + border: 1px solid transparent; + border-radius: 5px; + color: white; + background-color: $background-tertiary; + font-weight: lighter; + user-select: auto; + + &::selection { + background: $text-normal; + } + + &[disabled] { + background: none; + } + } } } } @@ -230,6 +267,12 @@ @Component({ name: 'neko-settings' }) export default class extends Vue { + private broadcast_url: string = ''; + + get admin() { + return this.$accessor.user.admin + } + get connected() { return this.$accessor.connected } @@ -282,6 +325,27 @@ return this.$accessor.settings.keyboard_layout } + get broadcast_is_active() { + return this.$accessor.settings.broadcast_is_active + } + + set broadcast_is_active(value: boolean) { + if (value) { + this.$accessor.settings.broadcastCreate(this.broadcast_url) + } else { + this.$accessor.settings.broadcastDestroy() + } + } + + get broadcast_url_remote() { + return this.$accessor.settings.broadcast_url + } + + @Watch('broadcast_url_remote', { immediate: true }) + onBroadcastUrlChange() { + this.broadcast_url = this.broadcast_url_remote + } + set keyboard_layout(value: string) { this.$accessor.settings.setKeyboardLayout(value) this.$accessor.remote.changeKeyboard() diff --git a/client/src/locale/en-us.ts b/client/src/locale/en-us.ts index 2cb49328..2f910381 100644 --- a/client/src/locale/en-us.ts +++ b/client/src/locale/en-us.ts @@ -61,6 +61,8 @@ export const setting = { ignore_emotes: 'Ignore Emotes', chat_sound: 'Play Chat Sound', keyboard_layout: 'Change Keyboard Layout', + broadcast_is_active: 'Broadcast Enabled', + broadcast_url: 'RTMP url', } export const connection = { diff --git a/client/src/neko/events.ts b/client/src/neko/events.ts index 92987513..b2bb0435 100644 --- a/client/src/neko/events.ts +++ b/client/src/neko/events.ts @@ -38,6 +38,11 @@ export const EVENT = { RESOLUTION: 'screen/resolution', SET: 'screen/set', }, + BROADCAST: { + STATUS: "broadcast/status", + CREATE: "broadcast/create", + DESTROY: "broadcast/destroy", + }, ADMIN: { BAN: 'admin/ban', KICK: 'admin/kick', @@ -60,6 +65,7 @@ export type WebSocketEvents = | SignalEvents | ChatEvents | ScreenEvents + | BroadcastEvents | AdminEvents export type ControlEvents = @@ -76,6 +82,11 @@ export type SignalEvents = typeof EVENT.SIGNAL.ANSWER | typeof EVENT.SIGNAL.PROV export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE export type ScreenEvents = typeof EVENT.SCREEN.CONFIGURATIONS | typeof EVENT.SCREEN.RESOLUTION | typeof EVENT.SCREEN.SET +export type BroadcastEvents = + | typeof EVENT.BROADCAST.STATUS + | typeof EVENT.BROADCAST.CREATE + | typeof EVENT.BROADCAST.DESTROY + export type AdminEvents = | typeof EVENT.ADMIN.BAN | typeof EVENT.ADMIN.KICK diff --git a/client/src/neko/index.ts b/client/src/neko/index.ts index e1f5b832..5913175d 100644 --- a/client/src/neko/index.ts +++ b/client/src/neko/index.ts @@ -18,6 +18,7 @@ import { ControlClipboardPayload, ScreenConfigurationsPayload, ScreenResolutionPayload, + BroadcastStatusPayload, AdminPayload, AdminTargetPayload, } from './messages' @@ -326,6 +327,13 @@ export class NekoClient extends BaseClient implements EventEmitter { }) } + ///////////////////////////// + // Broadcast Events + ///////////////////////////// + protected [EVENT.BROADCAST.STATUS](payload: BroadcastStatusPayload) { + this.$accessor.settings.broadcastStatus(payload) + } + ///////////////////////////// // Admin Events ///////////////////////////// diff --git a/client/src/neko/messages.ts b/client/src/neko/messages.ts index 27c737bd..3530c7c3 100644 --- a/client/src/neko/messages.ts +++ b/client/src/neko/messages.ts @@ -37,6 +37,8 @@ export type WebSocketPayloads = | ScreenResolutionPayload | ScreenConfigurationsPayload | AdminPayload + | BroadcastStatusPayload + | BroadcastCreatePayload export interface WebSocketMessage { event: WebSocketEvents | string @@ -177,6 +179,18 @@ export interface ScreenConfigurationsPayload { configurations: ScreenConfigurations } +/* + BROADCAST PAYLOADS +*/ +export interface BroadcastCreatePayload { + url: string +} + +export interface BroadcastStatusPayload { + url: string + isActive: boolean +} + /* ADMIN PAYLOADS */ diff --git a/client/src/store/settings.ts b/client/src/store/settings.ts index eb1295db..96c0c56e 100644 --- a/client/src/store/settings.ts +++ b/client/src/store/settings.ts @@ -1,5 +1,6 @@ import { getterTree, mutationTree, actionTree } from 'typed-vuex' import { get, set } from '~/utils/localstorage' +import { EVENT } from '~/neko/events' import { accessor } from '~/store' export const namespaced = true @@ -18,6 +19,9 @@ export const state = () => { keyboard_layout: get('keyboard_layout', 'us'), keyboard_layouts_list: {} as KeyboardLayouts, + + broadcast_is_active: false, + broadcast_url: "", } } @@ -57,6 +61,10 @@ export const mutations = mutationTree(state, { setKeyboardLayoutsList(state, value: KeyboardLayouts) { state.keyboard_layouts_list = value }, + setBroadcastStatus(state, { url, isActive }) { + state.broadcast_url = url, + state.broadcast_is_active = isActive + }, }) export const actions = actionTree( @@ -71,5 +79,15 @@ export const actions = actionTree( }) .catch(console.error) }, + + broadcastStatus({ getters }, { url, isActive }) { + accessor.settings.setBroadcastStatus({ url, isActive }) + }, + broadcastCreate({ getters }, url: string) { + $client.sendMessage(EVENT.BROADCAST.CREATE, { url }) + }, + broadcastDestroy({ getters }) { + $client.sendMessage(EVENT.BROADCAST.DESTROY) + }, }, ) diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index de16131b..12c1b8a0 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -12,9 +12,12 @@ services: - SYS_ADMIN environment: DISPLAY: :99.0 - NEKO_SCREEN: '1280x720@30' + NEKO_SCREEN: '1920x1080@30' NEKO_PASSWORD: neko NEKO_PASSWORD_ADMIN: admin NEKO_BIND: :8080 NEKO_EPR: 52000-52010 - NEKO_NAT1TO1: 192.168.1.20 \ No newline at end of file + NEKO_NAT1TO1: 192.168.1.20 + NEKO_BROADCAST: 'true' + NEKO_RTMP: 'rtmp://192.168.1.20/live/neko' + diff --git a/server/internal/broadcast/manager.go b/server/internal/broadcast/manager.go index 753b53bc..8937ffab 100644 --- a/server/internal/broadcast/manager.go +++ b/server/internal/broadcast/manager.go @@ -3,6 +3,7 @@ package broadcast import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "n.eko.moe/neko/internal/gst" "n.eko.moe/neko/internal/types/config" ) @@ -10,34 +11,70 @@ import ( type BroadcastManager struct { logger zerolog.Logger pipeline *gst.Pipeline - config *config.Broadcast + remote *config.Remote + enabled bool + url string } -func New(config *config.Broadcast) *BroadcastManager { +func New(remote *config.Remote) *BroadcastManager { return &BroadcastManager{ - logger: log.With().Str("module", "remote").Logger(), - config: config, + logger: log.With().Str("module", "remote").Logger(), + remote: remote, + enabled: false, + url: "", } } func (manager *BroadcastManager) Start() { + if !manager.enabled || manager.IsActive() { + return + } + var err error manager.pipeline, err = gst.CreateRTMPPipeline( - manager.config.Device, - manager.config.Display, - manager.config.RTMP, + manager.remote.Device, + manager.remote.Display, + manager.url, ) + + manager.logger.Info(). + Str("audio_device", manager.remote.Device). + Str("video_display", manager.remote.Display). + Str("rtmp_pipeline_src", manager.pipeline.Src). + Msgf("RTMP pipeline is starting...") + if err != nil { manager.logger.Panic().Err(err).Msg("unable to create rtmp pipeline") + return } - manager.pipeline.Start() + manager.pipeline.Play() } -func (manager *BroadcastManager) Shutdown() error { - if manager.pipeline != nil { - manager.pipeline.Stop() +func (manager *BroadcastManager) Stop() { + if !manager.IsActive() { + return } - return nil + manager.pipeline.Stop() + manager.pipeline = nil +} + +func (manager *BroadcastManager) IsActive() bool { + return manager.pipeline != nil +} + +func (manager *BroadcastManager) Create(url string) { + manager.url = url + manager.enabled = true + manager.Start() +} + +func (manager *BroadcastManager) Destroy() { + manager.Stop() + manager.enabled = false +} + +func (manager *BroadcastManager) GetUrl() string { + return manager.url } diff --git a/server/internal/gst/gst.c b/server/internal/gst/gst.c index 1ce97f44..924787fb 100644 --- a/server/internal/gst/gst.c +++ b/server/internal/gst/gst.c @@ -84,6 +84,10 @@ void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId) { gst_element_set_state(pipeline, GST_STATE_PLAYING); } +void gstreamer_send_play_pipeline(GstElement *pipeline) { + gst_element_set_state(pipeline, GST_STATE_PLAYING); +} + void gstreamer_send_stop_pipeline(GstElement *pipeline) { gst_element_set_state(pipeline, GST_STATE_NULL); } diff --git a/server/internal/gst/gst.go b/server/internal/gst/gst.go index 1c0eb90d..e2679de5 100644 --- a/server/internal/gst/gst.go +++ b/server/internal/gst/gst.go @@ -68,7 +68,8 @@ func init() { func CreateRTMPPipeline(pipelineDevice string, pipelineDisplay string, pipelineRTMP string) (*Pipeline, error) { video := fmt.Sprintf(videoSrc, pipelineDisplay) audio := fmt.Sprintf(audioSrc, pipelineDevice) - return CreatePipeline(fmt.Sprintf("%s ! x264enc ! flv. ! %s ! faac ! flv. ! flvmux name='flv' ! rtmpsink location='%s'", video, audio, pipelineRTMP), "", 0) + + return CreatePipeline(fmt.Sprintf("flvmux name=mux ! rtmpsink location='%s live=1' %s voaacenc ! mux. %s x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! mux.", pipelineRTMP, audio, video), "", 0) } // CreateAppPipeline creates a GStreamer Pipeline @@ -228,6 +229,11 @@ func (p *Pipeline) Start() { C.gstreamer_send_start_pipeline(p.Pipeline, C.int(p.id)) } +// Play starts the GStreamer Pipeline +func (p *Pipeline) Play() { + C.gstreamer_send_play_pipeline(p.Pipeline) +} + // Stop stops the GStreamer Pipeline func (p *Pipeline) Stop() { C.gstreamer_send_stop_pipeline(p.Pipeline) diff --git a/server/internal/gst/gst.h b/server/internal/gst/gst.h index 943e8b98..7c6965f4 100644 --- a/server/internal/gst/gst.h +++ b/server/internal/gst/gst.h @@ -11,6 +11,7 @@ extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int GstElement *gstreamer_send_create_pipeline(char *pipeline); void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId); +void gstreamer_send_play_pipeline(GstElement *pipeline); void gstreamer_send_stop_pipeline(GstElement *pipeline); void gstreamer_send_start_mainloop(void); void gstreamer_init(void); diff --git a/server/internal/remote/manager.go b/server/internal/remote/manager.go index 4b89d854..3ed2f005 100644 --- a/server/internal/remote/manager.go +++ b/server/internal/remote/manager.go @@ -18,19 +18,21 @@ type RemoteManager struct { video *gst.Pipeline audio *gst.Pipeline config *config.Remote + broadcast types.BroadcastManager cleanup *time.Ticker shutdown chan bool emmiter events.EventEmmiter streaming bool } -func New(config *config.Remote) *RemoteManager { +func New(config *config.Remote, broadcast types.BroadcastManager) *RemoteManager { return &RemoteManager{ logger: log.With().Str("module", "remote").Logger(), cleanup: time.NewTicker(1 * time.Second), shutdown: make(chan bool), emmiter: events.New(), config: config, + broadcast: broadcast, streaming: false, } } @@ -44,7 +46,16 @@ func (manager *RemoteManager) AudioCodec() string { } func (manager *RemoteManager) Start() { + xorg.Display(manager.config.Display) + + if !xorg.ValidScreenSize(manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate) { + manager.logger.Warn().Msgf("invalid screen option %dx%d@%d", manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate) + } else if err := xorg.ChangeScreenSize(manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate); err != nil { + manager.logger.Warn().Err(err).Msg("unable to change screen size") + } + manager.createPipelines() + manager.broadcast.Start() go func() { defer func() { @@ -70,6 +81,8 @@ func (manager *RemoteManager) Shutdown() error { manager.logger.Info().Msgf("remote shutting down") manager.video.Stop() manager.audio.Stop() + manager.broadcast.Stop() + manager.cleanup.Stop() manager.shutdown <- true return nil @@ -88,6 +101,8 @@ func (manager *RemoteManager) OnAudioFrame(listener func(sample types.Sample)) { } func (manager *RemoteManager) StartStream() { + manager.createPipelines() + manager.logger.Info(). Str("video_display", manager.config.Display). Str("video_codec", manager.config.VideoCodec). @@ -98,15 +113,6 @@ func (manager *RemoteManager) StartStream() { Str("screen_resolution", fmt.Sprintf("%dx%d@%d", manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate)). Msgf("Pipelines starting...") - xorg.Display(manager.config.Display) - - if !xorg.ValidScreenSize(manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate) { - manager.logger.Warn().Msgf("invalid screen option %dx%d@%d", manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate) - } else if err := xorg.ChangeScreenSize(manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate); err != nil { - manager.logger.Warn().Err(err).Msg("unable to change screen size") - } - - manager.createPipelines() manager.video.Start() manager.audio.Start() manager.streaming = true @@ -140,7 +146,7 @@ func (manager *RemoteManager) createPipelines() { manager.config.AudioParams, ) if err != nil { - manager.logger.Panic().Err(err).Msg("unable to screate audio pipeline") + manager.logger.Panic().Err(err).Msg("unable to create audio pipeline") } } @@ -150,8 +156,12 @@ func (manager *RemoteManager) ChangeResolution(width int, height int, rate int) } manager.video.Stop() + manager.broadcast.Stop() + defer func() { manager.video.Start() + manager.broadcast.Start() + manager.logger.Info().Msg("starting video pipeline...") }() @@ -159,17 +169,16 @@ func (manager *RemoteManager) ChangeResolution(width int, height int, rate int) return err } - video, err := gst.CreateAppPipeline( + var err error + manager.video, err = gst.CreateAppPipeline( manager.config.VideoCodec, manager.config.Display, manager.config.VideoParams, ) - if err != nil { manager.logger.Panic().Err(err).Msg("unable to create new video pipeline") } - manager.video = video return nil } diff --git a/server/internal/types/broadcast.go b/server/internal/types/broadcast.go new file mode 100644 index 00000000..fee3fd52 --- /dev/null +++ b/server/internal/types/broadcast.go @@ -0,0 +1,10 @@ +package types + +type BroadcastManager interface { + Start() + Stop() + IsActive() bool + Create(url string) + Destroy() + GetUrl() string +} diff --git a/server/internal/types/config/broadcast.go b/server/internal/types/config/broadcast.go deleted file mode 100644 index 35c35024..00000000 --- a/server/internal/types/config/broadcast.go +++ /dev/null @@ -1,48 +0,0 @@ -package config - -import ( - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -type Broadcast struct { - Enabled bool - Display string - Device string - AudioParams string - VideoParams string - RTMP string -} - -func (Broadcast) Init(cmd *cobra.Command) error { - cmd.PersistentFlags().Bool("broadcast", false, "use PCMA audio codec") - if err := viper.BindPFlag("broadcast", cmd.PersistentFlags().Lookup("broadcast")); err != nil { - return err - } - - cmd.PersistentFlags().String("rtmp", "", "RMTP url for broadcasting") - if err := viper.BindPFlag("rtmp", cmd.PersistentFlags().Lookup("rtmp")); err != nil { - return err - } - - cmd.PersistentFlags().String("cast_audio", "", "audio codec parameters to use for broadcasting") - if err := viper.BindPFlag("cast_audio", cmd.PersistentFlags().Lookup("cast_audio")); err != nil { - return err - } - - cmd.PersistentFlags().String("cast_video", "", "video codec parameters to use for broadcasting") - if err := viper.BindPFlag("cast_video", cmd.PersistentFlags().Lookup("cast_video")); err != nil { - return err - } - - return nil -} - -func (s *Broadcast) Set() { - s.Enabled = viper.GetBool("broadcast") - s.Display = viper.GetString("display") - s.Device = viper.GetString("device") - s.AudioParams = viper.GetString("cast_audio") - s.VideoParams = viper.GetString("cast_video") - s.RTMP = viper.GetString("rtmp") -} diff --git a/server/internal/types/event/events.go b/server/internal/types/event/events.go index 7b9d31ba..b97c49c8 100644 --- a/server/internal/types/event/events.go +++ b/server/internal/types/event/events.go @@ -36,6 +36,12 @@ const ( SCREEN_SET = "screen/set" ) +const ( + BORADCAST_STATUS = "broadcast/status" + BORADCAST_CREATE = "broadcast/create" + BORADCAST_DESTROY = "broadcast/destroy" +) + const ( ADMIN_BAN = "admin/ban" ADMIN_KICK = "admin/kick" diff --git a/server/internal/types/message/messages.go b/server/internal/types/message/messages.go index 8113e332..fcf04f30 100644 --- a/server/internal/types/message/messages.go +++ b/server/internal/types/message/messages.go @@ -110,3 +110,14 @@ type ScreenConfigurations struct { Event string `json:"event"` Configurations map[int]types.ScreenConfiguration `json:"configurations"` } + +type BroadcastStatus struct { + Event string `json:"event"` + URL string `json:"url"` + IsActive bool `json:"isActive"` +} + +type BroadcastCreate struct { + Event string `json:"event"` + URL string `json:"url"` +} diff --git a/server/internal/websocket/broadcast.go b/server/internal/websocket/broadcast.go new file mode 100644 index 00000000..c2d6a884 --- /dev/null +++ b/server/internal/websocket/broadcast.go @@ -0,0 +1,56 @@ +package websocket + +import ( + "n.eko.moe/neko/internal/types" + "n.eko.moe/neko/internal/types/event" + "n.eko.moe/neko/internal/types/message" +) + +func (h *MessageHandler) boradcastCreate(session types.Session, payload *message.BroadcastCreate) error { + if !session.Admin() { + h.logger.Debug().Msg("user not admin") + return nil + } + + h.broadcast.Create(payload.URL) + + if err := h.boradcastStatus(session); err != nil { + return err + } + + return nil +} + +func (h *MessageHandler) boradcastDestroy(session types.Session) error { + if !session.Admin() { + h.logger.Debug().Msg("user not admin") + return nil + } + + h.broadcast.Destroy() + + if err := h.boradcastStatus(session); err != nil { + return err + } + + return nil +} + +func (h *MessageHandler) boradcastStatus(session types.Session) error { + if !session.Admin() { + h.logger.Debug().Msg("user not admin") + return nil + } + + if err := session.Send( + message.BroadcastStatus{ + Event: event.BORADCAST_STATUS, + IsActive: h.broadcast.IsActive(), + URL: h.broadcast.GetUrl(), + }); err != nil { + h.logger.Warn().Err(err).Msgf("sending event %s has failed", event.BORADCAST_STATUS) + return err + } + + return nil +} diff --git a/server/internal/websocket/handler.go b/server/internal/websocket/handler.go index 5693c970..af829c82 100644 --- a/server/internal/websocket/handler.go +++ b/server/internal/websocket/handler.go @@ -13,12 +13,13 @@ import ( ) type MessageHandler struct { - logger zerolog.Logger - sessions types.SessionManager - webrtc types.WebRTCManager - remote types.RemoteManager - banned map[string]bool - locked bool + logger zerolog.Logger + sessions types.SessionManager + webrtc types.WebRTCManager + remote types.RemoteManager + broadcast types.BroadcastManager + banned map[string]bool + locked bool } func (h *MessageHandler) Connected(id string, socket *WebSocket) (bool, string, error) { @@ -123,6 +124,16 @@ func (h *MessageHandler) Message(id string, raw []byte) error { return h.screenSet(id, session, payload) }), "%s failed", header.Event) + // Boradcast Events + case event.BORADCAST_CREATE: + payload := &message.BroadcastCreate{} + return errors.Wrapf( + utils.Unmarshal(payload, raw, func() error { + return h.boradcastCreate(session, payload) + }), "%s failed", header.Event) + case event.BORADCAST_DESTROY: + return errors.Wrapf(h.boradcastDestroy(session), "%s failed", header.Event) + // Admin Events case event.ADMIN_LOCK: return errors.Wrapf(h.adminLock(id, session), "%s failed", header.Event) diff --git a/server/internal/websocket/session.go b/server/internal/websocket/session.go index 23d71cdb..9151aa23 100644 --- a/server/internal/websocket/session.go +++ b/server/internal/websocket/session.go @@ -17,6 +17,11 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error if err := h.screenConfigurations(id, session); err != nil { return err } + + // send broadcast status if admin + if err := h.boradcastStatus(session); err != nil { + return err + } } return nil diff --git a/server/internal/websocket/websocket.go b/server/internal/websocket/websocket.go index c7d85239..77e9651d 100644 --- a/server/internal/websocket/websocket.go +++ b/server/internal/websocket/websocket.go @@ -16,26 +16,27 @@ import ( "n.eko.moe/neko/internal/utils" ) -func New(sessions types.SessionManager, remote types.RemoteManager, webrtc types.WebRTCManager, conf *config.WebSocket) *WebSocketHandler { +func New(sessions types.SessionManager, remote types.RemoteManager, broadcast types.BroadcastManager, webrtc types.WebRTCManager, conf *config.WebSocket) *WebSocketHandler { logger := log.With().Str("module", "websocket").Logger() return &WebSocketHandler{ - logger: logger, - conf: conf, - sessions: sessions, - remote: remote, - upgrader: websocket.Upgrader{ + logger: logger, + conf: conf, + sessions: sessions, + remote: remote, + upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, }, handler: &MessageHandler{ - logger: logger.With().Str("subsystem", "handler").Logger(), - remote: remote, - sessions: sessions, - webrtc: webrtc, - banned: make(map[string]bool), - locked: false, + logger: logger.With().Str("subsystem", "handler").Logger(), + remote: remote, + broadcast: broadcast, + sessions: sessions, + webrtc: webrtc, + banned: make(map[string]bool), + locked: false, }, } } @@ -44,13 +45,13 @@ func New(sessions types.SessionManager, remote types.RemoteManager, webrtc types const pingPeriod = 60 * time.Second type WebSocketHandler struct { - logger zerolog.Logger - upgrader websocket.Upgrader - sessions types.SessionManager - remote types.RemoteManager - conf *config.WebSocket - handler *MessageHandler - shutdown chan bool + logger zerolog.Logger + upgrader websocket.Upgrader + sessions types.SessionManager + remote types.RemoteManager + conf *config.WebSocket + handler *MessageHandler + shutdown chan bool } func (ws *WebSocketHandler) Start() error { diff --git a/server/neko.go b/server/neko.go index 155abf3f..7d4dabbe 100644 --- a/server/neko.go +++ b/server/neko.go @@ -6,6 +6,7 @@ import ( "os/signal" "runtime" + "n.eko.moe/neko/internal/broadcast" "n.eko.moe/neko/internal/http" "n.eko.moe/neko/internal/remote" "n.eko.moe/neko/internal/session" @@ -107,6 +108,7 @@ type Neko struct { server *http.Server sessionManager *session.SessionManager remoteManager *remote.RemoteManager + broadcastManager *broadcast.BroadcastManager webRTCManager *webrtc.WebRTCManager webSocketHandler *websocket.WebSocketHandler } @@ -116,8 +118,9 @@ func (neko *Neko) Preflight() { } func (neko *Neko) Start() { + broadcastManager := broadcast.New(neko.Remote) - remoteManager := remote.New(neko.Remote) + remoteManager := remote.New(neko.Remote, broadcastManager) remoteManager.Start() sessionManager := session.New(remoteManager) @@ -125,7 +128,7 @@ func (neko *Neko) Start() { webRTCManager := webrtc.New(sessionManager, remoteManager, neko.WebRTC) webRTCManager.Start() - webSocketHandler := websocket.New(sessionManager, remoteManager, webRTCManager, neko.WebSocket) + webSocketHandler := websocket.New(sessionManager, remoteManager, broadcastManager, webRTCManager, neko.WebSocket) webSocketHandler.Start() server := http.New(neko.Server, webSocketHandler)