## Problem and outcome / 问题与结果 Sharing could stall at Browser encoder handoff or retain a pause after a video binding was replaced. Source/audio changes could also overlap offer/answer negotiation, while Native recovery could request redundant keyframes. This phase repairs those existing owners, drains capture output during bounded shutdown, and normalizes configured Site origins consistently with Browser serialization. ## Release notes ### 中文 这是一次稳定性更新,重点改善分享中的画面连续性,以及暂停、换源和恢复播放。 - 修复浏览器编码复用中的重复帧率限制和交接等待,减少由这些问题造成的卡顿。 - 修复换源和音频变更时的协商重叠,并防止已关闭连接的异步操作干扰恢复。 - 修复房主暂停期间重连后无法恢复播放,以及编码复用回退后仍停在暂停状态的问题;正常取消播放不再被当作媒体故障。 - 减少原生采集的重复关键帧请求,并修复停止采集时输出未排空导致的退出等待问题。 - 修复部分国际化域名和 IPv6 站点地址的识别,并改善窄屏下分享设置项的文字排版。 升级 Piik App 或自部署 Server 后刷新页面即可,无需修改配置。部分设备和网络环境下报告的开播失败、偶发断流仍需现场日志确认,本次更新不代表所有此类反馈均已解决。 ### English This stability update improves picture continuity and recovery when pausing, changing sources, or resuming playback. - Fix duplicate frame-rate limiting and unnecessary handoff waits in Browser encoding reuse, reducing stalls caused by these issues. - Serialize negotiation across source and audio changes, and prevent asynchronous work on closed connections from interfering with recovery. - Fix playback remaining paused after reconnecting while the Host is paused, or after falling back from shared encoding. Normal playback cancellation is no longer treated as a media failure. - Avoid redundant Native capture keyframe requests and drain pending capture output during bounded shutdown. - Fix recognition of some internationalized domain names and IPv6 Site addresses, and improve sharing-setting labels on narrow screens. Update Piik App or your self-hosted Server, then refresh the page. No configuration changes are required. Some device- and network-specific reports of sharing startup failure or intermittent stream loss still need matching diagnostics; this update does not claim to resolve every reported case. ## Verification / 验证 - `npm run check`: 76 test files / 1,152 tests, TypeScript and production Web build passed. - Real Chrome VP8/H264 checks with two viewers passed live/paused profile changes, quiet-source recovery, source replacement and ordinary-sender fallback after producer failure. - New pause-owner regressions fail against the previous implementation; actual Viewer effects preserve same-binding local pause and resume replacement bindings. - Independent cumulative Browser and Native/config review found no confirmed release blocker. - `npm run check:go`: formatting, Go tests/vet, Windows/Linux builds and Windows native capture compile/probes passed. Darwin packaging and Go race detection run on the native CI runners. - Windows App H264 and VP8 Host gates passed decoded playback, live/paused quality changes, quiet/background-source recovery, source replacement, failure cleanup and subsequent sharing; experiment-owned processes, ports and profiles were cleaned up. - Documentation, repository hygiene and release-note extraction passed. ## Impact / 影响 Patch release v1.6.7. Browser/server v23, Native control v9, capture v7 and SQLite schema 2 remain unchanged. Existing WebRTC adaptation and route policy remain unchanged; no quality floor is introduced. Interaction and Windows x86 prototypes are excluded. Managed Server restart interrupts active media and preserves room authority. Physical macOS/Linux capture, broad public-network endurance and unmatched reporter environments remain outside local acceptance.
146 lines
4.8 KiB
Go
146 lines
4.8 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
|
)
|
|
|
|
// LocalOptions describes App-selected Local composition. It never reads the
|
|
// process environment.
|
|
type LocalOptions struct {
|
|
// Port 0 selects the default 8787.
|
|
Port int
|
|
PublicAddress string
|
|
PublicOrigin string
|
|
AllowedAddresses []string
|
|
SiteAccessPassword string
|
|
STUNURLs []string
|
|
NATPredictionSTUNURLs []string
|
|
}
|
|
|
|
func Local(options LocalOptions) (Config, error) {
|
|
port := options.Port
|
|
if port == 0 {
|
|
port = defaultPort
|
|
}
|
|
if port < 1 || port > maxPort {
|
|
return Config{}, fmt.Errorf("Local server port must be an integer between 1 and %d", maxPort)
|
|
}
|
|
publicAddress := ""
|
|
if options.PublicAddress != "" {
|
|
parsed, err := localIPv4(options.PublicAddress, "public address")
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
if parsed == "0.0.0.0" || strings.HasPrefix(parsed, "127.") {
|
|
return Config{}, errors.New("Local server public address must be reachable from the LAN")
|
|
}
|
|
publicAddress = parsed
|
|
} else if options.PublicOrigin == "" {
|
|
return Config{}, errors.New("Local server public address must be reachable from the LAN")
|
|
}
|
|
allowedAddresses := make([]string, 0, 1+len(options.AllowedAddresses))
|
|
if publicAddress != "" {
|
|
allowedAddresses = append(allowedAddresses, publicAddress)
|
|
}
|
|
for _, address := range options.AllowedAddresses {
|
|
normalized, err := localIPv4(address, "allowed address")
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
if !slices.Contains(allowedAddresses, normalized) {
|
|
allowedAddresses = append(allowedAddresses, normalized)
|
|
}
|
|
}
|
|
|
|
// Copy non-nil lists so configured-empty prediction does not derive auxiliary URLs.
|
|
stunURLs := append([]string{}, options.STUNURLs...)
|
|
natPredictionSTUNURLs := append([]string{}, options.NATPredictionSTUNURLs...)
|
|
if len(stunURLs)+len(natPredictionSTUNURLs) > protocol.MaxIceServerURLs ||
|
|
!allValidStunURLs(stunURLs) ||
|
|
!allValidStunURLs(natPredictionSTUNURLs) ||
|
|
(len(natPredictionSTUNURLs) != 0 &&
|
|
len(natPredictionSTUNURLs) != protocol.MaxNatPredictionAuxiliaryStunURLs) ||
|
|
(len(natPredictionSTUNURLs) > 0 && len(stunURLs) == 0) {
|
|
return Config{}, errors.New("Local STUN URLs are invalid")
|
|
}
|
|
|
|
// Local is loopback/LAN HTTP; its origin always spells out the port.
|
|
origin := func(host string) string { return fmt.Sprintf("http://%s:%d", host, port) }
|
|
var publicBaseURL *url.URL
|
|
if options.PublicOrigin != "" {
|
|
parsed, err := publicHTTPSOrigin(options.PublicOrigin)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
publicBaseURL = parsed
|
|
} else {
|
|
publicBaseURL = &url.URL{Scheme: "http", Host: fmt.Sprintf("%s:%d", publicAddress, port), Path: "/"}
|
|
}
|
|
|
|
allowedOrigins := map[string]struct{}{
|
|
origin("localhost"): {},
|
|
origin("127.0.0.1"): {},
|
|
}
|
|
for _, address := range allowedAddresses {
|
|
allowedOrigins[origin(address)] = struct{}{}
|
|
}
|
|
allowedOrigins[Origin(publicBaseURL)] = struct{}{}
|
|
|
|
return Config{
|
|
Env: EnvironmentProduction,
|
|
Port: port,
|
|
ListenHost: "0.0.0.0",
|
|
PublicBaseURL: publicBaseURL,
|
|
AllowedOrigins: allowedOrigins,
|
|
SiteAccessPassword: options.SiteAccessPassword,
|
|
MaxViewersPerRoom: protocol.MaxViewersPerRoomLimit,
|
|
EndpointMediaCopyCapacity: protocol.DefaultEndpointMediaCopyCapacity,
|
|
STUNURLs: stunURLs,
|
|
NATPredictionEnabled: len(natPredictionSTUNURLs) == protocol.MaxNatPredictionAuxiliaryStunURLs,
|
|
NATPredictionSTUNURLs: natPredictionSTUNURLs,
|
|
}, nil
|
|
}
|
|
|
|
func allValidStunURLs(values []string) bool {
|
|
for _, value := range values {
|
|
if !protocol.ValidStunURLValue(value) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// publicHTTPSOrigin uses one error message for all invalid origins.
|
|
func publicHTTPSOrigin(value string) (*url.URL, error) {
|
|
parsed, err := url.Parse(value)
|
|
if err != nil || parsed.Scheme != "https" || parsed.Host == "" ||
|
|
hasUserinfo(parsed) || (parsed.Path != "" && parsed.Path != "/") ||
|
|
parsed.RawQuery != "" || parsed.Fragment != "" {
|
|
return nil, errors.New("Local public origin must be an HTTPS origin")
|
|
}
|
|
host := normalizedHost(parsed)
|
|
if host == "" {
|
|
return nil, errors.New("Local public origin must be an HTTPS origin")
|
|
}
|
|
return &url.URL{Scheme: "https", Host: host, Path: "/"}, nil
|
|
}
|
|
|
|
// Node's isIP(value) === 4 accepts only a dotted
|
|
// quad, so an IPv4-mapped IPv6 literal such as "::ffff:192.168.1.10" — which
|
|
// net.ParseIP(...).To4() does resolve — is rejected here as well.
|
|
func localIPv4(value string, name string) (string, error) {
|
|
address := strings.TrimFunc(value, protocol.IsJSWhitespace)
|
|
parsed := net.ParseIP(address)
|
|
if parsed == nil || parsed.To4() == nil || strings.ContainsRune(address, ':') {
|
|
return "", fmt.Errorf("Local server %s must be an IPv4 address", name)
|
|
}
|
|
return address, nil
|
|
}
|