Files
olcrtc/internal/app/session/session.go
2026-05-07 14:28:32 +03:00

387 lines
11 KiB
Go

// Package session wires runtime configuration to application mode entrypoints.
package session
import (
"context"
"errors"
"fmt"
"github.com/openlibrecommunity/olcrtc/internal/carrier"
"github.com/openlibrecommunity/olcrtc/internal/carrier/builtin"
"github.com/openlibrecommunity/olcrtc/internal/client"
"github.com/openlibrecommunity/olcrtc/internal/link"
"github.com/openlibrecommunity/olcrtc/internal/link/direct"
"github.com/openlibrecommunity/olcrtc/internal/server"
"github.com/openlibrecommunity/olcrtc/internal/transport"
"github.com/openlibrecommunity/olcrtc/internal/transport/datachannel"
"github.com/openlibrecommunity/olcrtc/internal/transport/seichannel"
"github.com/openlibrecommunity/olcrtc/internal/transport/videochannel"
"github.com/openlibrecommunity/olcrtc/internal/transport/vp8channel"
)
const (
modeSRV = "srv"
modeCNC = "cnc"
)
var (
// ErrRoomIDRequired indicates that a room id is required for the selected carrier.
ErrRoomIDRequired = errors.New("room ID required (use -id <id>)")
// ErrModeRequired indicates that mode is not one of the supported values.
ErrModeRequired = errors.New("mode required (use -mode srv or -mode cnc)")
// ErrCarrierRequired indicates that no carrier was selected.
ErrCarrierRequired = errors.New(
"carrier required (use -carrier telemost, -carrier jazz or -carrier wbstream)")
// ErrUnsupportedCarrier indicates that carrier is not registered.
ErrUnsupportedCarrier = errors.New("unsupported carrier")
// ErrUnsupportedLink indicates that link is not registered.
ErrUnsupportedLink = errors.New("unsupported link")
// ErrUnsupportedTransport indicates that transport is not registered.
ErrUnsupportedTransport = errors.New("unsupported transport")
// ErrLinkRequired indicates that link is not provided.
ErrLinkRequired = errors.New("link required (use -link direct)")
// ErrTransportRequired indicates that transport is not provided.
ErrTransportRequired = errors.New(
"transport required (use -transport datachannel, -transport videochannel, " +
"-transport seichannel or -transport vp8channel)")
// ErrKeyRequired indicates that encryption key is not provided.
ErrKeyRequired = errors.New("key required (use -key <hex>)")
// ErrDNSServerRequired indicates that dns server is not provided.
ErrDNSServerRequired = errors.New("dns server required (use -dns 1.1.1.1:53)")
// ErrVideoWidthRequired indicates that video width is required for videochannel.
ErrVideoWidthRequired = errors.New("video width required for videochannel (use -video-w)")
// ErrVideoHeightRequired indicates that video height is required for videochannel.
ErrVideoHeightRequired = errors.New("video height required for videochannel (use -video-h)")
// ErrVideoFPSRequired indicates that video fps is required for videochannel.
ErrVideoFPSRequired = errors.New("video fps required for videochannel (use -video-fps)")
// ErrVideoBitrateRequired indicates that video bitrate is required for videochannel.
ErrVideoBitrateRequired = errors.New(
"video bitrate required for videochannel (use -video-bitrate)")
// ErrVideoHWRequired indicates that video hardware acceleration is required.
ErrVideoHWRequired = errors.New(
"video hardware acceleration required for videochannel (use -video-hw none/nvenc)")
// ErrVideoCodecInvalid indicates that the video codec is not valid.
ErrVideoCodecInvalid = errors.New(
"invalid video codec for videochannel (use -video-codec qrcode or -video-codec tile)")
// ErrTileCodecDimensions indicates that tile codec requires 1080x1080 dimensions.
ErrTileCodecDimensions = errors.New("tile codec requires -video-w 1080 -video-h 1080")
// ErrVP8FPSRequired indicates that vp8 fps is required for vp8channel.
ErrVP8FPSRequired = errors.New("vp8 fps required for vp8channel (use -vp8-fps)")
// ErrVP8BatchSizeRequired indicates that vp8 batch size is required for vp8channel.
ErrVP8BatchSizeRequired = errors.New("vp8 batch size required for vp8channel (use -vp8-batch)")
// ErrSEIFPSRequired indicates that seichannel fps is required.
ErrSEIFPSRequired = errors.New("fps required for seichannel (use -fps)")
// ErrSEIBatchSizeRequired indicates that seichannel batch size is required.
ErrSEIBatchSizeRequired = errors.New("batch size required for seichannel (use -batch)")
// ErrSEIFragmentSizeRequired indicates that seichannel fragment size is required.
ErrSEIFragmentSizeRequired = errors.New("fragment size required for seichannel (use -frag)")
// ErrSEIAckTimeoutRequired indicates that seichannel ack timeout is required.
ErrSEIAckTimeoutRequired = errors.New("ack timeout required for seichannel (use -ack-ms)")
// ErrSOCKSHostRequired indicates that socks host is required for cnc mode.
ErrSOCKSHostRequired = errors.New("socks host required for cnc mode (use -socks-host)")
// ErrSOCKSPortRequired indicates that socks port is required for cnc mode.
ErrSOCKSPortRequired = errors.New("socks port required for cnc mode (use -socks-port)")
// ErrClientIDRequired indicates that client ID is required.
ErrClientIDRequired = errors.New("client ID required (use -client-id <id>)")
)
// Config holds runtime session settings.
type Config struct {
Mode string
Link string
Transport string
Carrier string
RoomID string
ClientID string
KeyHex string
SOCKSHost string
SOCKSPort int
DNSServer string
SOCKSProxyAddr string
SOCKSProxyPort int
VideoWidth int
VideoHeight int
VideoFPS int
VideoBitrate string
VideoHW string
VideoQRSize int
VideoQRRecovery string
VideoCodec string
VideoTileModule int
VideoTileRS int
VP8FPS int
VP8BatchSize int
SEIFPS int
SEIBatchSize int
SEIFragmentSize int
SEIAckTimeoutMS int
}
// RegisterDefaults registers built-in carriers and transports.
func RegisterDefaults() {
builtin.Register()
link.Register("direct", direct.New)
transport.Register("datachannel", datachannel.New)
transport.Register("videochannel", videochannel.New)
transport.Register("seichannel", seichannel.New)
transport.Register("vp8channel", vp8channel.New)
}
// Validate verifies that the runtime config refers to registered components and all required fields are present.
func Validate(cfg Config) error {
if err := validateMode(cfg); err != nil {
return err
}
if err := validateCarrier(cfg); err != nil {
return err
}
if err := validateLink(cfg); err != nil {
return err
}
if err := validateTransportRegistration(cfg); err != nil {
return err
}
if err := validateCommon(cfg); err != nil {
return err
}
if err := validateTransportConfig(cfg); err != nil {
return err
}
return validateModeConfig(cfg)
}
func validateMode(cfg Config) error {
if cfg.Mode == "" || (cfg.Mode != modeSRV && cfg.Mode != modeCNC) {
return ErrModeRequired
}
return nil
}
func validateCarrier(cfg Config) error {
if cfg.Carrier == "" {
return ErrCarrierRequired
}
for _, c := range carrier.Available() {
if cfg.Carrier == c {
return nil
}
}
return fmt.Errorf("%w: %s (available: %v)", ErrUnsupportedCarrier, cfg.Carrier, carrier.Available())
}
func validateLink(cfg Config) error {
if cfg.Link == "" {
return ErrLinkRequired
}
for _, l := range link.Available() {
if cfg.Link == l {
return nil
}
}
return fmt.Errorf("%w: %s (available: %v)", ErrUnsupportedLink, cfg.Link, link.Available())
}
func validateTransportRegistration(cfg Config) error {
if cfg.Transport == "" {
return ErrTransportRequired
}
for _, t := range transport.Available() {
if cfg.Transport == t {
return nil
}
}
return fmt.Errorf("%w: %s (available: %v)", ErrUnsupportedTransport, cfg.Transport, transport.Available())
}
func validateCommon(cfg Config) error {
if cfg.RoomID == "" && cfg.Carrier != "jazz" {
return ErrRoomIDRequired
}
if cfg.ClientID == "" {
return ErrClientIDRequired
}
if cfg.KeyHex == "" {
return ErrKeyRequired
}
if cfg.DNSServer == "" {
return ErrDNSServerRequired
}
return nil
}
func validateTransportConfig(cfg Config) error {
switch cfg.Transport {
case "videochannel":
return validateVideoChannel(cfg)
case "vp8channel":
return validateVP8Channel(cfg)
case "seichannel":
return validateSEIChannel(cfg)
default:
return nil
}
}
func validateVideoCodec(cfg Config) error {
if cfg.VideoCodec != "" && cfg.VideoCodec != "qrcode" && cfg.VideoCodec != "tile" {
return ErrVideoCodecInvalid
}
if cfg.VideoCodec == "tile" && (cfg.VideoWidth != 1080 || cfg.VideoHeight != 1080) {
return ErrTileCodecDimensions
}
return nil
}
func validateVideoChannel(cfg Config) error {
if cfg.VideoWidth == 0 {
return ErrVideoWidthRequired
}
if cfg.VideoHeight == 0 {
return ErrVideoHeightRequired
}
if cfg.VideoFPS == 0 {
return ErrVideoFPSRequired
}
if cfg.VideoBitrate == "" {
return ErrVideoBitrateRequired
}
if cfg.VideoHW == "" {
return ErrVideoHWRequired
}
return validateVideoCodec(cfg)
}
func validateVP8Channel(cfg Config) error {
if cfg.VP8FPS == 0 {
return ErrVP8FPSRequired
}
if cfg.VP8BatchSize == 0 {
return ErrVP8BatchSizeRequired
}
return nil
}
func validateSEIChannel(cfg Config) error {
if cfg.SEIFPS == 0 {
return ErrSEIFPSRequired
}
if cfg.SEIBatchSize == 0 {
return ErrSEIBatchSizeRequired
}
if cfg.SEIFragmentSize == 0 {
return ErrSEIFragmentSizeRequired
}
if cfg.SEIAckTimeoutMS == 0 {
return ErrSEIAckTimeoutRequired
}
return nil
}
func validateModeConfig(cfg Config) error {
if cfg.Mode != modeCNC {
return nil
}
if cfg.SOCKSHost == "" {
return ErrSOCKSHostRequired
}
if cfg.SOCKSPort == 0 {
return ErrSOCKSPortRequired
}
return nil
}
// Run starts the configured mode.
func Run(ctx context.Context, cfg Config) error {
roomURL := buildRoomURL(cfg.Carrier, cfg.RoomID)
switch cfg.Mode {
case modeSRV:
if err := server.Run(
ctx,
cfg.Link,
cfg.Transport,
cfg.Carrier,
roomURL,
cfg.KeyHex,
cfg.ClientID,
cfg.DNSServer,
cfg.SOCKSProxyAddr,
cfg.SOCKSProxyPort,
cfg.VideoWidth,
cfg.VideoHeight,
cfg.VideoFPS,
cfg.VideoBitrate,
cfg.VideoHW,
cfg.VideoQRSize,
cfg.VideoQRRecovery,
cfg.VideoCodec,
cfg.VideoTileModule,
cfg.VideoTileRS,
cfg.VP8FPS,
cfg.VP8BatchSize,
cfg.SEIFPS,
cfg.SEIBatchSize,
cfg.SEIFragmentSize,
cfg.SEIAckTimeoutMS,
); err != nil {
return fmt.Errorf("server: %w", err)
}
return nil
case modeCNC:
if err := client.Run(
ctx,
cfg.Link,
cfg.Transport,
cfg.Carrier,
roomURL,
cfg.KeyHex,
cfg.ClientID,
fmt.Sprintf("%s:%d", cfg.SOCKSHost, cfg.SOCKSPort),
cfg.DNSServer,
"",
"",
cfg.VideoWidth,
cfg.VideoHeight,
cfg.VideoFPS,
cfg.VideoBitrate,
cfg.VideoHW,
cfg.VideoQRSize,
cfg.VideoQRRecovery,
cfg.VideoCodec,
cfg.VideoTileModule,
cfg.VideoTileRS,
cfg.VP8FPS,
cfg.VP8BatchSize,
cfg.SEIFPS,
cfg.SEIBatchSize,
cfg.SEIFragmentSize,
cfg.SEIAckTimeoutMS,
); err != nil {
return fmt.Errorf("client: %w", err)
}
return nil
default:
return ErrModeRequired
}
}
func buildRoomURL(carrierName, roomID string) string {
switch carrierName {
case "telemost":
return "https://telemost.yandex.ru/j/" + roomID
case "jazz":
if roomID == "" {
return "any"
}
return roomID
case "wbstream":
return roomID
default:
return roomID
}
}