11 Commits

Author SHA1 Message Date
1b84c7e7ba fix invalid errors. 2024-07-20 14:48:54 +02:00
21a4b2b797 autostart broadcast only if url is set. 2024-06-18 23:35:22 +02:00
5e96bca296 update readme. 2024-06-17 23:20:42 +02:00
c78d797fe7 fix typo. 2024-06-17 23:16:48 +02:00
57596315e9 broadcast_autostart as config option, #398. 2024-06-17 23:14:12 +02:00
0d7887e9d2 workaround for firefox read clipboard, #373.
Firefox 122+ incorrectly reports that it can read the clipboard but it can't instead it hangs when reading clipboard, until user clicks on the page and the click itself is not handled by the page at all, also the clipboard reads always fail with "Clipboard read operation is not allowed."
2024-06-16 22:55:13 +02:00
978fd8977d google does not archive chrome 111 anymore. 2024-06-16 22:28:32 +02:00
4ab5901ba9 sync clipboard only if in focus #373. 2024-06-16 22:27:46 +02:00
11a862f101 update docs. 2024-05-19 23:17:17 +02:00
b938a4e09e update docs. 2024-05-19 17:07:52 +02:00
e26e4d2004 Add zh_TW Traditional Chinese locale (#388) 2024-04-17 15:49:57 +02:00
19 changed files with 213 additions and 59 deletions

View File

@ -3,7 +3,9 @@ FROM $BASE_IMAGE
# latest working version with EGL: 111.0.5563.146, revert when resolved
# 112.0.5615.49 fails: https://github.com/VirtualGL/virtualgl/issues/229
ARG SRC_URL="https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_111.0.5563.146-1_amd64.deb"
# google does not provide a direct link to the deb file anymore
# ARG SRC_URL="https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_111.0.5563.146-1_amd64.deb"
ARG SRC_URL="https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb"
#
# install google chrome

View File

@ -119,30 +119,23 @@
}
}
@media only screen and (max-width: 1024px) {
html,
body {
overflow-y: auto !important;
width: auto !important;
height: auto !important;
}
@media only screen and (max-width: 600px) {
#neko.expanded {
.neko-main {
transform: translateX(calc(-100% + 65px));
body > p {
display: none;
}
#neko {
position: relative;
flex-direction: column;
max-height: initial !important;
.neko-main .video-container {
height: 100vh;
video {
display: none;
}
}
.neko-menu {
height: 100vh;
width: 100% !important;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 65px;
width: calc(100% - 65px);
}
}
}

View File

@ -24,7 +24,7 @@
<style lang="scss" scoped>
.connect {
position: fixed;
position: absolute;
top: 0;
left: 0;
right: 0;

View File

@ -313,7 +313,15 @@
}
get clipboard_read_available() {
return 'clipboard' in navigator && typeof navigator.clipboard.readText === 'function'
return (
'clipboard' in navigator &&
typeof navigator.clipboard.readText === 'function' &&
// Firefox 122+ incorrectly reports that it can read the clipboard but it can't
// instead it hangs when reading clipboard, until user clicks on the page
// and the click itself is not handled by the page at all, also the clipboard
// reads always fail with "Clipboard read operation is not allowed."
navigator.userAgent.indexOf('Firefox') == -1
)
}
get clipboard_write_available() {
@ -630,7 +638,7 @@
}
async syncClipboard() {
if (this.clipboard_read_available) {
if (this.clipboard_read_available && window.document.hasFocus()) {
try {
const text = await navigator.clipboard.readText()
if (this.clipboard !== text) {

View File

@ -9,6 +9,7 @@ import * as ko from './ko-kr'
import * as fi from './fi-fi'
import * as ru from './ru-ru'
import * as cn from './zh-cn'
import * as tw from './zh-tw'
export const messages = {
en,
@ -22,4 +23,5 @@ export const messages = {
fi,
ru,
cn,
tw,
}

127
client/src/locale/zh-tw.ts Normal file
View File

@ -0,0 +1,127 @@
export const logout = '登出'
export const unsupported = '您的網頁瀏覽器不支援 WebRTC'
export const admin_loggedin = '您已經以管理員身份登入'
export const you = '您'
export const somebody = '某人'
export const send_a_message = '傳送訊息'
export const side = {
chat: '聊天',
files: '檔案',
settings: '設定',
}
export const connect = {
login_title: '請登入',
invitation_title: '您已被邀請進入此房間',
displayname: '輸入您的顯示名稱',
password: '密碼',
connect: '連線',
error: '登入錯誤',
empty_displayname: '顯示名稱不能為空。',
}
export const context = {
ignore: '忽略',
unignore: '取消忽略',
mute: '靜音',
unmute: '解除靜音',
release: '強制釋放控制',
take: '強制接管控制',
give: '移交控制',
kick: '踢出',
ban: '封鎖 IP',
confirm: {
kick_title: '踢出 {name}',
kick_text: '您確定要踢出 {name} 嗎?',
ban_title: '封鎖 {name}',
ban_text: '您是否要封鎖 {name}?封鎖後需要重新啟動伺服器才能取消封鎖。',
mute_title: '靜音 {name}',
mute_text: '您確定要將 {name} 設為靜音嗎?',
unmute_title: '解除靜音 {name}',
unmute_text: '您是否要解除 {name} 的靜音?',
button_yes: '是',
button_cancel: '取消',
},
}
export const controls = {
release: '釋放控制',
request: '請求控制',
lock: '鎖定控制',
unlock: '解鎖控制',
has: '您擁有控制權',
hasnot: '您沒有控制權',
}
export const locks = {
control: {
lock: '鎖定控制(對使用者)',
unlock: '解鎖控制(對使用者)',
locked: '已鎖定控制(對使用者)',
unlocked: '已解鎖控制(對使用者)',
notif_locked: '已鎖定使用者控制',
notif_unlocked: '已解鎖使用者控制',
},
login: {
lock: '鎖定房間(對使用者)',
unlock: '解鎖房間(對使用者)',
locked: '房間已鎖定(對使用者)',
unlocked: '房間已解鎖(對使用者)',
notif_locked: '已鎖定房間',
notif_unlocked: '已解鎖房間',
},
file_transfer: {
lock: '鎖定檔案傳輸(對使用者)',
unlock: '解鎖檔案傳輸(對使用者)',
locked: '檔案傳輸已鎖定(對使用者)',
unlocked: '檔案傳輸已解鎖(對使用者)',
notif_locked: '已鎖定檔案傳輸',
notif_unlocked: '已解鎖檔案傳輸',
},
}
export const setting = {
scroll: '滾動靈敏度',
scroll_invert: '反向滾動',
autoplay: '自動播放影片',
ignore_emotes: '忽略表情符號',
chat_sound: '播放聊天音效',
keyboard_layout: '鍵盤配置',
broadcast_title: '直播',
}
export const connection = {
logged_out: '您已登出。',
reconnecting: '正在重新連線…',
connected: '已連線',
disconnected: '已斷線',
kicked: '您已被移出此房間。',
button_confirm: '確定',
}
export const notifications = {
connected: '{name} 已連線',
disconnected: '{name} 已斷線',
controls_taken: '{name} 接管了控制權',
controls_taken_force: '強制接管控制權',
controls_taken_steal: '從 {name} 奪取了控制權',
controls_released: '{name} 釋放了控制權',
controls_released_force: '強制釋放控制權',
controls_released_steal: '從 {name} 強制釋放控制權',
controls_given: '將控制權交給 {name}',
controls_has: '{name} 擁有控制權',
controls_has_alt: '但我已通知對方您有意接管',
controls_requesting: '{name} 正在請求控制權',
resolution: '將解析度改為 {width}x{height}@{rate}',
banned: '已封鎖 {name}',
kicked: '已踢出 {name}',
muted: '已將 {name} 靜音',
unmuted: '已解除 {name} 的靜音',
}
export const files = {
downloads: '下載',
uploads: '上傳',
upload_here: '點選或拖曳檔案至此上傳',
}

View File

@ -6,6 +6,7 @@
- Added nvidia support for firefox.
- Added `?lang=<lang>` parameter to the URL, which will set the language of the interface (by @mbattista).
- Added `?show_side=1` and `?mute_chat=1` parameter to the URL, for chat mute and show side (by @mbattista).
- Added `NEKO_BROADCAST_AUTOSTART` to automatically start or do not start broadcasting when the room is created. By default, it is set to `true` because it was the previous behavior.
### Bugs
- Fix incorrect version sorting for chromium, microsoft-edge, opera and ungoogledchromium.

View File

@ -107,9 +107,11 @@ nat1to1: <ip>
#### `NEKO_BROADCAST_PIPELINE`:
- Makes it possible to create custom gstreamer pipeline used for broadcasting, strings `{url}`, `{device}` and `{display}` will be replaced.
#### `NEKO_BROADCAST_URL`:
- Set a default URL for broadcast streams. Setting this value will automatically enable broadcasting when n.eko starts. It can be disabled/changed later by admins in the GUI.
- Set a default URL for broadcast streams. It can be disabled/changed later by admins in the GUI.
- e.g. `rtmp://<your-server>:1935/ingest/<stream-key>`
#### `NEKO_BROADCAST_AUTOSTART`:
- Automatically start broadcasting when neko starts and broadcast_url is set.
- e.g. `true`
### Server
#### `NEKO_BIND`:

View File

@ -4,15 +4,18 @@ Neko UI loads, but you don't see the screen, and it gives you `connection timeou
## Test your client
Some browser may block WebRTC access by default. You can check if it is enabled by going to `about:webrtc` or `chrome://webrtc-internals` in your browser.
Some browsers may block WebRTC access by default. You can check if it is enabled by going to `about:webrtc` or `chrome://webrtc-internals` in your browser.
Check if your extensions are not blocking WebRTC access. For example, Privacy Badger or Private Internet Access blocks WebRTC by default.
Check if your extensions are not blocking WebRTC access. Following extensions are known to block or does not work properly with WebRTC:
- Privacy Badger
- Private Internet Access
- PIA VPN (even if disabled)
Test whether your client [supports](https://www.webrtc-experiment.com/DetectRTC/) and can [connect to WebRTC](https://www.webcasts.com/webrtc/).
## Networking
Most problems are networking related.
If you are absolutely sure, that your client is working correctly, then most likely your networking is not set up correctly.
### Check if your ports are correctly exposed using docker
@ -59,6 +62,13 @@ Then try to type on one end, you should see characters on the other side.
If it does not work for you, then most likely your port forwarding is not working correctly. Or your ISP is blocking traffic.
If you get [`Command 'nc' not found.`](https://command-not-found.com/nc) error, you can install `netcat` package using:
```shell
sudo apt-get install netcat
```
### Check if your external IP was determined correctly
One of the first logs, when the server starts, writes down your external IP that will be sent to your clients to connect to.
@ -67,6 +77,8 @@ One of the first logs, when the server starts, writes down your external IP that
docker-compose logs neko | grep nat_ips
```
Note: Some newer versions of docker-compose use `docker compose` instead of `docker-compose`.
You should see this:
```

View File

@ -22,7 +22,7 @@ type BroacastManagerCtx struct {
started bool
}
func broadcastNew(pipelineFn func(url string) (string, error), defaultUrl string) *BroacastManagerCtx {
func broadcastNew(pipelineFn func(url string) (string, error), url string, started bool) *BroacastManagerCtx {
logger := log.With().
Str("module", "capture").
Str("submodule", "broadcast").
@ -31,8 +31,8 @@ func broadcastNew(pipelineFn func(url string) (string, error), defaultUrl string
return &BroacastManagerCtx{
logger: logger,
pipelineFn: pipelineFn,
url: defaultUrl,
started: defaultUrl != "",
url: url,
started: started && url != "",
}
}

View File

@ -31,7 +31,7 @@ func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCt
// sinks
broadcast: broadcastNew(func(url string) (string, error) {
return NewBroadcastPipeline(config.AudioDevice, config.Display, config.BroadcastPipeline, url)
}, config.BroadcastUrl),
}, config.BroadcastUrl, config.BroadcastAutostart),
audio: streamSinkNew(config.AudioCodec, func() (string, error) {
return NewAudioPipeline(config.AudioCodec, config.AudioDevice, config.AudioPipeline, config.AudioBitrate)
}, "audio"),

View File

@ -34,8 +34,9 @@ type Capture struct {
AudioPipeline string
// broadcast
BroadcastPipeline string
BroadcastUrl string
BroadcastPipeline string
BroadcastUrl string
BroadcastAutostart bool
}
func (Capture) Init(cmd *cobra.Command) error {
@ -155,11 +156,16 @@ func (Capture) Init(cmd *cobra.Command) error {
return err
}
cmd.PersistentFlags().String("broadcast_url", "", "URL for broadcasting, setting this value will automatically enable broadcasting")
cmd.PersistentFlags().String("broadcast_url", "", "a default default URL for broadcast streams, can be disabled/changed later by admins in the GUI")
if err := viper.BindPFlag("broadcast_url", cmd.PersistentFlags().Lookup("broadcast_url")); err != nil {
return err
}
cmd.PersistentFlags().Bool("broadcast_autostart", true, "automatically start broadcasting when neko starts and broadcast_url is set")
if err := viper.BindPFlag("broadcast_autostart", cmd.PersistentFlags().Lookup("broadcast_autostart")); err != nil {
return err
}
return nil
}
@ -247,4 +253,5 @@ func (s *Capture) Set() {
s.BroadcastPipeline = viper.GetString("broadcast_pipeline")
s.BroadcastUrl = viper.GetString("broadcast_url")
s.BroadcastAutostart = viper.GetBool("broadcast_autostart")
}

View File

@ -46,9 +46,9 @@ const (
)
const (
BORADCAST_STATUS = "broadcast/status"
BORADCAST_CREATE = "broadcast/create"
BORADCAST_DESTROY = "broadcast/destroy"
BROADCAST_STATUS = "broadcast/status"
BROADCAST_CREATE = "broadcast/create"
BROADCAST_DESTROY = "broadcast/destroy"
)
const (

View File

@ -175,7 +175,7 @@ func (h *MessageHandler) adminGive(id string, session types.Session, payload *me
ID: id,
Target: payload.ID,
}, nil); err != nil {
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_LOCKED)
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_GIVE)
return err
}
@ -207,7 +207,7 @@ func (h *MessageHandler) adminMute(id string, session types.Session, payload *me
Target: target.ID(),
ID: id,
}, nil); err != nil {
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_UNMUTE)
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_MUTE)
return err
}

View File

@ -6,7 +6,7 @@ import (
"m1k1o/neko/internal/types/message"
)
func (h *MessageHandler) boradcastCreate(session types.Session, payload *message.BroadcastCreate) error {
func (h *MessageHandler) broadcastCreate(session types.Session, payload *message.BroadcastCreate) error {
broadcast := h.capture.Broadcast()
if !session.Admin() {
@ -44,14 +44,14 @@ func (h *MessageHandler) boradcastCreate(session types.Session, payload *message
}
}
if err := h.boradcastStatus(nil); err != nil {
if err := h.broadcastStatus(nil); err != nil {
return err
}
return nil
}
func (h *MessageHandler) boradcastDestroy(session types.Session) error {
func (h *MessageHandler) broadcastDestroy(session types.Session) error {
broadcast := h.capture.Broadcast()
if !session.Admin() {
@ -70,18 +70,18 @@ func (h *MessageHandler) boradcastDestroy(session types.Session) error {
broadcast.Stop()
if err := h.boradcastStatus(nil); err != nil {
if err := h.broadcastStatus(nil); err != nil {
return err
}
return nil
}
func (h *MessageHandler) boradcastStatus(session types.Session) error {
func (h *MessageHandler) broadcastStatus(session types.Session) error {
broadcast := h.capture.Broadcast()
msg := message.BroadcastStatus{
Event: event.BORADCAST_STATUS,
Event: event.BROADCAST_STATUS,
IsActive: broadcast.Started(),
URL: broadcast.Url(),
}
@ -89,7 +89,7 @@ func (h *MessageHandler) boradcastStatus(session types.Session) error {
// if no session, broadcast change
if session == nil {
if err := h.sessions.AdminBroadcast(msg, nil); err != nil {
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.BORADCAST_STATUS)
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.BROADCAST_STATUS)
return err
}
@ -102,7 +102,7 @@ func (h *MessageHandler) boradcastStatus(session types.Session) error {
}
if err := session.Send(msg); err != nil {
h.logger.Warn().Err(err).Msgf("sending event %s has failed", event.BORADCAST_STATUS)
h.logger.Warn().Err(err).Msgf("sending event %s has failed", event.BROADCAST_STATUS)
return err
}

View File

@ -17,7 +17,7 @@ func (h *MessageHandler) chat(id string, session types.Session, payload *message
Content: payload.Content,
ID: id,
}, nil); err != nil {
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_RELEASE)
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CHAT_MESSAGE)
return err
}
return nil
@ -34,7 +34,7 @@ func (h *MessageHandler) chatEmote(id string, session types.Session, payload *me
Emote: payload.Emote,
ID: id,
}, nil); err != nil {
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_RELEASE)
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CHAT_EMOTE)
return err
}
return nil

View File

@ -115,7 +115,7 @@ func (h *MessageHandler) controlGive(id string, session types.Session, payload *
ID: id,
Target: payload.ID,
}, nil); err != nil {
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_LOCKED)
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_GIVE)
return err
}

View File

@ -148,15 +148,15 @@ 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:
// Broadcast Events
case event.BROADCAST_CREATE:
payload := &message.BroadcastCreate{}
return errors.Wrapf(
utils.Unmarshal(payload, raw, func() error {
return h.boradcastCreate(session, payload)
return h.broadcastCreate(session, payload)
}), "%s failed", header.Event)
case event.BORADCAST_DESTROY:
return errors.Wrapf(h.boradcastDestroy(session), "%s failed", header.Event)
case event.BROADCAST_DESTROY:
return errors.Wrapf(h.broadcastDestroy(session), "%s failed", header.Event)
// Admin Events
case event.ADMIN_LOCK:

View File

@ -30,7 +30,7 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error
}
// send broadcast status if admin
if err := h.boradcastStatus(session); err != nil {
if err := h.broadcastStatus(session); err != nil {
return err
}
}
@ -78,7 +78,7 @@ func (h *MessageHandler) SessionConnected(id string, session types.Session) erro
Event: event.MEMBER_CONNECTED,
Member: session.Member(),
}, nil); err != nil {
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_RELEASE)
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.MEMBER_CONNECTED)
return err
}