diff --git a/.m1k1o/base/Dockerfile b/.m1k1o/base/Dockerfile
index 8db7947..7371cb5 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 6497810..1bd2179 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 b5ae941..612ee18 100644
--- a/client/src/components/settings.vue
+++ b/client/src/components/settings.vue
@@ -48,6 +48,19 @@
+
+
+ {{ $t('setting.broadcast_is_active') }}
+
+
+
+ {{ $t('setting.broadcast_url') }}
+
+
+
@@ -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 2cb4932..2f91038 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 9298751..b2bb043 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 e1f5b83..5913175 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 27c737b..3530c7c 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 eb1295d..96c0c56 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 de16131..12c1b8a 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 753b53b..8937ffa 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 1ce97f4..924787f 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 1c0eb90..e2679de 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 943e8b9..7c6965f 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 4b89d85..3ed2f00 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 0000000..fee3fd5
--- /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 35c3502..0000000
--- 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 7b9d31b..b97c49c 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 8113e33..fcf04f3 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 0000000..c2d6a88
--- /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 5693c97..af829c8 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 23d71cd..9151aa2 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 c7d8523..77e9651 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 155abf3..7d4dabb 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)