对接
This commit is contained in:
@@ -23,6 +23,8 @@ USER kra
|
|||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
# WebRTC 媒体 UDP 端口(KRA_LIVE_UDP_PORT,deploy.sh 默认 9504 并通过 -p 发布)
|
||||||
|
EXPOSE 9504/udp
|
||||||
VOLUME /data/conf
|
VOLUME /data/conf
|
||||||
|
|
||||||
CMD ["sh", "-c", "exec ./$BIN_NAME -conf /data/conf"]
|
CMD ["sh", "-c", "exec ./$BIN_NAME -conf /data/conf"]
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ UPLOADS_DIR="$BASE_DIR/uploads" # 上传文件目录 -> 容器内 /app/uploads
|
|||||||
FRONTEND_PORT=9501 # admin 管理后台 nginx 容器
|
FRONTEND_PORT=9501 # admin 管理后台 nginx 容器
|
||||||
BACKEND_PORT=9502 # 后端 API/WebSocket 容器
|
BACKEND_PORT=9502 # 后端 API/WebSocket 容器
|
||||||
LIVEWEB_PORT=9503 # live 用户端(PC 直播页) nginx 容器
|
LIVEWEB_PORT=9503 # live 用户端(PC 直播页) nginx 容器
|
||||||
|
LIVE_UDP_PORT="${KRA_LIVE_UDP_PORT:-9504}" # WebRTC 音视频固定 UDP 端口(与 9501-9503 顺延配套)
|
||||||
|
LIVE_PUBLIC_IP="${KRA_LIVE_PUBLIC_IP:-}" # 留空时从站点域名解析
|
||||||
|
LIVE_HOST="${KRA_LIVE_HOST:-live.fuhua91.com}"
|
||||||
NETWORK="live-net"
|
NETWORK="live-net"
|
||||||
BACKEND_NAME="live-backend"
|
BACKEND_NAME="live-backend"
|
||||||
FRONTEND_NAME="live-frontend"
|
FRONTEND_NAME="live-frontend"
|
||||||
@@ -49,6 +52,15 @@ if [ ! -f "$CONFIG_DIR/config.yaml" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -z "$LIVE_PUBLIC_IP" ]; then
|
||||||
|
LIVE_PUBLIC_IP="$(getent ahostsv4 "$LIVE_HOST" | awk 'NR == 1 { print $1 }')"
|
||||||
|
fi
|
||||||
|
if ! printf '%s' "$LIVE_PUBLIC_IP" | grep -Eq '^[0-9]+(\.[0-9]+){3}$'; then
|
||||||
|
echo "错误: 无法确定直播服务公网 IPv4,请通过 KRA_LIVE_PUBLIC_IP 显式设置"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> WebRTC 媒体地址: $LIVE_PUBLIC_IP:$LIVE_UDP_PORT/udp"
|
||||||
|
|
||||||
# 3. 登录 Harbor;未传密码时复用服务器现有的 Docker 登录态
|
# 3. 登录 Harbor;未传密码时复用服务器现有的 Docker 登录态
|
||||||
HARBOR_USER="${HARBOR_USER:-$HARBOR_USER_DEFAULT}"
|
HARBOR_USER="${HARBOR_USER:-$HARBOR_USER_DEFAULT}"
|
||||||
HARBOR_LOGGED_IN=false
|
HARBOR_LOGGED_IN=false
|
||||||
@@ -80,6 +92,9 @@ docker run -d --name "$BACKEND_NAME" \
|
|||||||
--restart always \
|
--restart always \
|
||||||
--network "$NETWORK" \
|
--network "$NETWORK" \
|
||||||
-p "$BACKEND_PORT:8000" \
|
-p "$BACKEND_PORT:8000" \
|
||||||
|
-p "$LIVE_UDP_PORT:$LIVE_UDP_PORT/udp" \
|
||||||
|
-e KRA_LIVE_PUBLIC_IP="$LIVE_PUBLIC_IP" \
|
||||||
|
-e KRA_LIVE_UDP_PORT="$LIVE_UDP_PORT" \
|
||||||
-v "$CONFIG_DIR:/data/conf" \
|
-v "$CONFIG_DIR:/data/conf" \
|
||||||
-v "$LOG_DIR:/app/logs" \
|
-v "$LOG_DIR:/app/logs" \
|
||||||
-v "$UPLOADS_DIR:/app/uploads" \
|
-v "$UPLOADS_DIR:/app/uploads" \
|
||||||
|
|||||||
+11
-3
@@ -146,12 +146,20 @@ go run ./tmp/live_acceptance verify
|
|||||||
|
|
||||||
## 网络配置
|
## 网络配置
|
||||||
|
|
||||||
默认使用 Pion 主机候选。生产环境可以配置:
|
默认使用 Pion 主机候选。本机开发无需额外配置;后端运行在 Docker、Kubernetes 或 NAT 后方时,推荐使用一个固定 UDP 端口并声明浏览器可访问的公网或局域网 IP:
|
||||||
|
|
||||||
|
```text
|
||||||
|
KRA_LIVE_PUBLIC_IP=203.0.113.10
|
||||||
|
KRA_LIVE_UDP_PORT=9504
|
||||||
|
```
|
||||||
|
|
||||||
|
容器必须同时发布同一个 UDP 端口,例如 `-p 9504:9504/udp`,主机防火墙和云安全组也必须放行 UDP 9504。`deploy/deploy.sh` 已自动发布该端口(默认 9504,与 9501-9503 顺延配套),并默认从 `live.fuhua91.com` 解析公网 IPv4;也可以在执行脚本前显式设置 `KRA_LIVE_PUBLIC_IP`。
|
||||||
|
|
||||||
|
若基础设施更适合端口范围,也可以不设置 `KRA_LIVE_UDP_PORT`,改用:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
KRA_LIVE_STUN_URL=stun:stun.example.com:3478
|
|
||||||
KRA_LIVE_UDP_MIN_PORT=40000
|
KRA_LIVE_UDP_MIN_PORT=40000
|
||||||
KRA_LIVE_UDP_MAX_PORT=49999
|
KRA_LIVE_UDP_MAX_PORT=49999
|
||||||
```
|
```
|
||||||
|
|
||||||
配置 UDP 端口范围后,需要在防火墙或容器网络中放行对应端口。
|
端口范围模式同样需要逐一发布和放行整个 UDP 范围。跨复杂 NAT、企业网络或移动网络时,可额外配置 `KRA_LIVE_STUN_URL=stun:stun.example.com:3478`;需要强制中继时应部署 TURN 服务。
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -29,6 +31,7 @@ type Manager struct {
|
|||||||
chat *bizchat.Usecase
|
chat *bizchat.Usecase
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
api *webrtc.API
|
api *webrtc.API
|
||||||
|
iceMux io.Closer
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
rooms map[string]*room
|
rooms map[string]*room
|
||||||
peers map[*melody.Session]*peer
|
peers map[*melody.Session]*peer
|
||||||
@@ -89,19 +92,57 @@ type chatSendPayload struct {
|
|||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPionAPI() (*webrtc.API, error) {
|
func newPionAPI() (*webrtc.API, io.Closer, error) {
|
||||||
settings := webrtc.SettingEngine{}
|
settings := webrtc.SettingEngine{}
|
||||||
|
fixedPort := envPort("KRA_LIVE_UDP_PORT")
|
||||||
minPort := envPort("KRA_LIVE_UDP_MIN_PORT")
|
minPort := envPort("KRA_LIVE_UDP_MIN_PORT")
|
||||||
maxPort := envPort("KRA_LIVE_UDP_MAX_PORT")
|
maxPort := envPort("KRA_LIVE_UDP_MAX_PORT")
|
||||||
|
if fixedPort > 0 && (minPort > 0 || maxPort > 0) {
|
||||||
|
return nil, nil, errors.New("KRA_LIVE_UDP_PORT cannot be combined with KRA_LIVE_UDP_MIN_PORT/KRA_LIVE_UDP_MAX_PORT")
|
||||||
|
}
|
||||||
if minPort > 0 || maxPort > 0 {
|
if minPort > 0 || maxPort > 0 {
|
||||||
if minPort == 0 || maxPort == 0 || minPort > maxPort {
|
if minPort == 0 || maxPort == 0 || minPort > maxPort {
|
||||||
return nil, fmt.Errorf("invalid KRA_LIVE_UDP_MIN_PORT/KRA_LIVE_UDP_MAX_PORT")
|
return nil, nil, fmt.Errorf("invalid KRA_LIVE_UDP_MIN_PORT/KRA_LIVE_UDP_MAX_PORT")
|
||||||
}
|
}
|
||||||
if err := settings.SetEphemeralUDPPortRange(minPort, maxPort); err != nil {
|
if err := settings.SetEphemeralUDPPortRange(minPort, maxPort); err != nil {
|
||||||
return nil, fmt.Errorf("configure live udp port range: %w", err)
|
return nil, nil, fmt.Errorf("configure live udp port range: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return webrtc.NewAPI(webrtc.WithSettingEngine(settings)), nil
|
publicIPs, err := livePublicIPs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if len(publicIPs) > 0 {
|
||||||
|
settings.SetNAT1To1IPs(publicIPs, webrtc.ICECandidateTypeHost)
|
||||||
|
}
|
||||||
|
var mux io.Closer
|
||||||
|
if fixedPort > 0 {
|
||||||
|
packetConn, listenErr := net.ListenPacket("udp4", fmt.Sprintf(":%d", fixedPort))
|
||||||
|
if listenErr != nil {
|
||||||
|
return nil, nil, fmt.Errorf("listen on live udp port %d: %w", fixedPort, listenErr)
|
||||||
|
}
|
||||||
|
udpMux := webrtc.NewICEUDPMux(nil, packetConn)
|
||||||
|
settings.SetICEUDPMux(udpMux)
|
||||||
|
mux = udpMux
|
||||||
|
}
|
||||||
|
return webrtc.NewAPI(webrtc.WithSettingEngine(settings)), mux, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func livePublicIPs() ([]string, error) {
|
||||||
|
value := strings.TrimSpace(os.Getenv("KRA_LIVE_PUBLIC_IP"))
|
||||||
|
if value == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
items := strings.Split(value, ",")
|
||||||
|
result := make([]string, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
item = strings.TrimSpace(item)
|
||||||
|
if net.ParseIP(item) == nil {
|
||||||
|
return nil, fmt.Errorf("invalid KRA_LIVE_PUBLIC_IP %q", item)
|
||||||
|
}
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func envPort(key string) uint16 {
|
func envPort(key string) uint16 {
|
||||||
@@ -133,7 +174,7 @@ func New(hub platformws.Hub, streams *bizlive.StreamUsecase, rooms *bizlive.Room
|
|||||||
if chat == nil {
|
if chat == nil {
|
||||||
return nil, nil, errors.New("live chat usecase is unavailable")
|
return nil, nil, errors.New("live chat usecase is unavailable")
|
||||||
}
|
}
|
||||||
api, err := newPionAPI()
|
api, iceMux, err := newPionAPI()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -141,12 +182,14 @@ func New(hub platformws.Hub, streams *bizlive.StreamUsecase, rooms *bizlive.Room
|
|||||||
logger = slog.Default()
|
logger = slog.Default()
|
||||||
}
|
}
|
||||||
logger = logger.With("mod", "live")
|
logger = logger.With("mod", "live")
|
||||||
m := &Manager{hub: hub, streams: streams, roomCatalog: rooms, chat: chat, logger: logger, api: api, rooms: make(map[string]*room), peers: make(map[*melody.Session]*peer), observers: make(map[*melody.Session]struct{}), stop: make(chan struct{})}
|
m := &Manager{hub: hub, streams: streams, roomCatalog: rooms, chat: chat, logger: logger, api: api, iceMux: iceMux, rooms: make(map[string]*room), peers: make(map[*melody.Session]*peer), observers: make(map[*melody.Session]struct{}), stop: make(chan struct{})}
|
||||||
hub.OnMessage(m.onMessage)
|
hub.OnMessage(m.onMessage)
|
||||||
hub.OnDisconnect(m.onDisconnect)
|
hub.OnDisconnect(m.onDisconnect)
|
||||||
go m.heartbeatLoop()
|
go m.heartbeatLoop()
|
||||||
logger.Info("live signaling manager initialized",
|
logger.Info("live signaling manager initialized",
|
||||||
"stun_configured", len(liveICEServers()) > 0,
|
"stun_configured", len(liveICEServers()) > 0,
|
||||||
|
"public_ips", strings.TrimSpace(os.Getenv("KRA_LIVE_PUBLIC_IP")),
|
||||||
|
"udp_port", envPort("KRA_LIVE_UDP_PORT"),
|
||||||
"udp_min_port", envPort("KRA_LIVE_UDP_MIN_PORT"),
|
"udp_min_port", envPort("KRA_LIVE_UDP_MIN_PORT"),
|
||||||
"udp_max_port", envPort("KRA_LIVE_UDP_MAX_PORT"))
|
"udp_max_port", envPort("KRA_LIVE_UDP_MAX_PORT"))
|
||||||
return m, m.Close, nil
|
return m, m.Close, nil
|
||||||
@@ -164,6 +207,11 @@ func (m *Manager) Close() {
|
|||||||
for _, p := range peers {
|
for _, p := range peers {
|
||||||
m.closePeer(p, "server_shutdown")
|
m.closePeer(p, "server_shutdown")
|
||||||
}
|
}
|
||||||
|
if m.iceMux != nil {
|
||||||
|
if err := m.iceMux.Close(); err != nil {
|
||||||
|
m.liveLogger().Warn("live UDP mux close failed", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) onMessage(session *melody.Session, raw []byte) {
|
func (m *Manager) onMessage(session *melody.Session, raw []byte) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package live
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -480,3 +481,29 @@ func TestClosedSupersededPeerConnectionDoesNotRemovePeer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var _ platformws.Hub = (*liveHubStub)(nil)
|
var _ platformws.Hub = (*liveHubStub)(nil)
|
||||||
|
|
||||||
|
func TestNewPionAPIRejectsConflictingUDPConfiguration(t *testing.T) {
|
||||||
|
t.Setenv("KRA_LIVE_UDP_PORT", "40000")
|
||||||
|
t.Setenv("KRA_LIVE_UDP_MIN_PORT", "40000")
|
||||||
|
t.Setenv("KRA_LIVE_UDP_MAX_PORT", "40010")
|
||||||
|
|
||||||
|
_, closer, err := newPionAPI()
|
||||||
|
if closer != nil {
|
||||||
|
_ = closer.Close()
|
||||||
|
}
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "cannot be combined") {
|
||||||
|
t.Fatalf("newPionAPI error = %v, want conflicting UDP configuration", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewPionAPIRejectsInvalidPublicIP(t *testing.T) {
|
||||||
|
t.Setenv("KRA_LIVE_PUBLIC_IP", "live.example.com")
|
||||||
|
|
||||||
|
_, closer, err := newPionAPI()
|
||||||
|
if closer != nil {
|
||||||
|
_ = closer.Close()
|
||||||
|
}
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "invalid KRA_LIVE_PUBLIC_IP") {
|
||||||
|
t.Fatalf("newPionAPI error = %v, want invalid public IP", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user