feat: ad mode gen for generate room

This commit is contained in:
zarazaex69
2026-05-08 17:29:53 +03:00
parent 4194ac9144
commit 46a7e64010
5 changed files with 237 additions and 81 deletions

View File

@@ -20,12 +20,16 @@ import (
"github.com/openlibrecommunity/olcrtc/internal/names"
)
// ErrDataDirRequired is returned when no data directory is specified.
var ErrDataDirRequired = errors.New("data directory required (use -data data)")
//nolint:gochecknoglobals // Tests replace the long-running session runner with a bounded function.
var runSession = session.Run
//nolint:gochecknoglobals // Tests replace gen runner with a stub.
var runGen func(config) error = execGen
type config struct {
mode string
link string
@@ -57,6 +61,7 @@ type config struct {
seiBatchSize int
seiFragmentSize int
seiAckTimeoutMS int
amount int
}
func main() {
@@ -83,6 +88,10 @@ func runWithArgs(args []string) error {
func runWithConfig(cfg config) error {
configureLogging(cfg.debug)
if cfg.mode == "gen" {
return runGen(cfg) //nolint:wrapcheck
}
if err := session.Validate(toSessionConfig(cfg)); err != nil {
return fmt.Errorf("validate config: %w", err)
}
@@ -121,6 +130,32 @@ func runWithConfig(cfg config) error {
}
}
func execGen(cfg config) error {
scfg := toSessionConfig(cfg)
if err := session.ValidateGen(scfg); err != nil {
return fmt.Errorf("validate gen config: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
errCh := make(chan error, 1)
go func() {
errCh <- session.Gen(ctx, scfg, func(id string) { fmt.Println(id) })
}()
select {
case <-sigCh:
cancel()
return waitForShutdown(errCh)
case err := <-errCh:
return err
}
}
func parseFlagsFrom(args []string, errorHandling flag.ErrorHandling) (config, error) {
cfg := config{}
fs := flag.NewFlagSet("olcrtc", errorHandling)
@@ -161,6 +196,7 @@ func parseFlagsFrom(args []string, errorHandling flag.ErrorHandling) (config, er
fs.IntVar(&cfg.seiBatchSize, "batch", 0, "Transport frames per tick for batched transports (seichannel)")
fs.IntVar(&cfg.seiFragmentSize, "frag", 0, "Fragment size in bytes for fragmented transports (seichannel)")
fs.IntVar(&cfg.seiAckTimeoutMS, "ack-ms", 0, "ACK timeout in milliseconds for reliable visual transports (seichannel)")
fs.IntVar(&cfg.amount, "amount", 0, "Number of rooms to generate (gen mode only)")
if err := fs.Parse(args); err != nil {
return cfg, fmt.Errorf("parse flags: %w", err)
@@ -232,6 +268,7 @@ func toSessionConfig(cfg config) session.Config {
SEIBatchSize: cfg.seiBatchSize,
SEIFragmentSize: cfg.seiFragmentSize,
SEIAckTimeoutMS: cfg.seiAckTimeoutMS,
Amount: cfg.amount,
}
}

View File

@@ -43,13 +43,15 @@ func TestToSessionConfig(t *testing.T) {
seiBatchSize: 3,
seiFragmentSize: 512,
seiAckTimeoutMS: 1500,
amount: 5,
}
got := toSessionConfig(cfg)
if got.Mode != cfg.mode || got.Carrier != "jazz" || got.SOCKSPort != cfg.socksPort ||
got.VideoTileRS != cfg.videoTileRS || got.VP8BatchSize != cfg.vp8BatchSize ||
got.SEIFPS != cfg.seiFPS || got.SEIBatchSize != cfg.seiBatchSize ||
got.SEIFragmentSize != cfg.seiFragmentSize || got.SEIAckTimeoutMS != cfg.seiAckTimeoutMS {
got.SEIFragmentSize != cfg.seiFragmentSize || got.SEIAckTimeoutMS != cfg.seiAckTimeoutMS ||
got.Amount != cfg.amount {
t.Fatalf("toSessionConfig() = %+v", got)
}
}
@@ -86,6 +88,7 @@ func TestParseFlagsFrom(t *testing.T) {
"-batch", "4",
"-frag", "512",
"-ack-ms", "1500",
"-amount", "7",
}, flag.ContinueOnError)
if err != nil {
t.Fatalf("parseFlagsFrom() error = %v", err)
@@ -93,7 +96,8 @@ func TestParseFlagsFrom(t *testing.T) {
if cfg.mode != "srv" || cfg.carrier != "telemost" || cfg.roomID != "room" ||
cfg.debug != true || cfg.videoCodec != "tile" || cfg.videoTileRS != 40 ||
cfg.vp8FPS != 24 || cfg.vp8BatchSize != 3 || cfg.seiFPS != 40 ||
cfg.seiBatchSize != 4 || cfg.seiFragmentSize != 512 || cfg.seiAckTimeoutMS != 1500 {
cfg.seiBatchSize != 4 || cfg.seiFragmentSize != 512 || cfg.seiAckTimeoutMS != 1500 ||
cfg.amount != 7 {
t.Fatalf("parseFlagsFrom() = %+v", cfg)
}
@@ -103,6 +107,41 @@ func TestParseFlagsFrom(t *testing.T) {
}
}
func TestRunGenModeValidationErrors(t *testing.T) {
session.RegisterDefaults()
if err := runWithConfig(config{mode: "gen"}); err == nil {
t.Fatal("runWithConfig(gen, no carrier) error = nil")
}
if err := runWithConfig(config{mode: "gen", carrier: "wbstream", dnsServer: "1.1.1.1:53"}); err == nil {
t.Fatal("runWithConfig(gen, amount=0) error = nil")
}
}
func TestRunGenModeCallsGen(t *testing.T) {
session.RegisterDefaults()
var collected []string
oldRunGen := runGen
t.Cleanup(func() { runGen = oldRunGen })
runGen = func(cfg config) error {
if cfg.carrier != "wbstream" || cfg.dnsServer != "1.1.1.1:53" || cfg.amount != 3 {
t.Fatalf("runGen cfg = %+v", cfg)
}
collected = append(collected, "ok")
return nil
}
err := runWithConfig(config{mode: "gen", carrier: "wbstream", dnsServer: "1.1.1.1:53", amount: 3})
if err != nil {
t.Fatalf("runWithConfig(gen) error = %v", err)
}
if len(collected) != 1 {
t.Fatalf("runGen called %d times, want 1", len(collected))
}
}
func TestRunWithConfigValidationAndDataDirErrors(t *testing.T) {
session.RegisterDefaults()
cfg := config{

View File

@@ -5,12 +5,16 @@ import (
"context"
"errors"
"fmt"
"slices"
"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/names"
"github.com/openlibrecommunity/olcrtc/internal/provider/jazz"
"github.com/openlibrecommunity/olcrtc/internal/provider/wbstream"
"github.com/openlibrecommunity/olcrtc/internal/server"
"github.com/openlibrecommunity/olcrtc/internal/transport"
"github.com/openlibrecommunity/olcrtc/internal/transport/datachannel"
@@ -22,8 +26,10 @@ import (
const (
modeSRV = "srv"
modeCNC = "cnc"
modeGen = "gen"
carrierJazz = "jazz"
carrierTelemost = "telemost"
carrierWBStream = "wbstream"
transportVideo = "videochannel"
transportVP8 = "vp8channel"
transportSEI = "seichannel"
@@ -37,7 +43,9 @@ 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)")
ErrModeRequired = errors.New("mode required (use -mode srv, -mode cnc or -mode gen)")
// ErrAmountRequired indicates that -amount is required for gen mode.
ErrAmountRequired = errors.New("amount required for gen mode (use -amount <n>)")
// ErrCarrierRequired indicates that no carrier was selected.
ErrCarrierRequired = errors.New(
"carrier required (use -carrier telemost, -carrier jazz or -carrier wbstream)")
@@ -128,6 +136,7 @@ type Config struct {
SEIBatchSize int
SEIFragmentSize int
SEIAckTimeoutMS int
Amount int
}
// RegisterDefaults registers built-in carriers and transports.
@@ -164,46 +173,42 @@ func Validate(cfg Config) error {
}
func validateMode(cfg Config) error {
if cfg.Mode == "" || (cfg.Mode != modeSRV && cfg.Mode != modeCNC) {
switch cfg.Mode {
case modeSRV, modeCNC, modeGen:
return nil
default:
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
}
if !slices.Contains(carrier.Available(), cfg.Carrier) {
return fmt.Errorf("%w: %s (available: %v)", ErrUnsupportedCarrier, cfg.Carrier, carrier.Available())
}
return fmt.Errorf("%w: %s (available: %v)", ErrUnsupportedCarrier, cfg.Carrier, carrier.Available())
return nil
}
func validateLink(cfg Config) error {
if cfg.Link == "" {
return ErrLinkRequired
}
for _, l := range link.Available() {
if cfg.Link == l {
return nil
}
if !slices.Contains(link.Available(), cfg.Link) {
return fmt.Errorf("%w: %s (available: %v)", ErrUnsupportedLink, cfg.Link, link.Available())
}
return fmt.Errorf("%w: %s (available: %v)", ErrUnsupportedLink, cfg.Link, link.Available())
return nil
}
func validateTransportRegistration(cfg Config) error {
if cfg.Transport == "" {
return ErrTransportRequired
}
for _, t := range transport.Available() {
if cfg.Transport == t {
return nil
}
if !slices.Contains(transport.Available(), cfg.Transport) {
return fmt.Errorf("%w: %s (available: %v)", ErrUnsupportedTransport, cfg.Transport, transport.Available())
}
return fmt.Errorf("%w: %s (available: %v)", ErrUnsupportedTransport, cfg.Transport, transport.Available())
return nil
}
func validateCommon(cfg Config) error {
@@ -387,9 +392,51 @@ func buildRoomURL(carrierName, roomID string) string {
return roomURLAny
}
return roomID
case "wbstream":
case carrierWBStream:
return roomID
default:
return roomID
}
}
// ValidateGen validates that the config contains enough fields to run gen mode.
func ValidateGen(cfg Config) error {
if cfg.Carrier == "" {
return ErrCarrierRequired
}
if !slices.Contains(carrier.Available(), cfg.Carrier) {
return fmt.Errorf("%w: %s (available: %v)", ErrUnsupportedCarrier, cfg.Carrier, carrier.Available())
}
if cfg.DNSServer == "" {
return ErrDNSServerRequired
}
if cfg.Amount < 1 {
return ErrAmountRequired
}
return nil
}
// Gen creates cfg.Amount rooms for the configured carrier and writes each room ID to out.
func Gen(ctx context.Context, cfg Config, out func(string)) error {
switch cfg.Carrier {
case carrierJazz:
for i := range cfg.Amount {
info, err := jazz.CreateRoom(ctx)
if err != nil {
return fmt.Errorf("gen jazz room %d: %w", i+1, err)
}
out(info.RoomID)
}
case carrierWBStream:
for i := range cfg.Amount {
roomID, err := wbstream.CreateRoom(ctx, names.Generate())
if err != nil {
return fmt.Errorf("gen wbstream room %d: %w", i+1, err)
}
out(roomID)
}
default:
return fmt.Errorf("%w: %s does not support room generation", ErrUnsupportedCarrier, cfg.Carrier)
}
return nil
}

View File

@@ -2,6 +2,7 @@
package session
import (
"context"
"errors"
"testing"
)
@@ -356,3 +357,71 @@ func TestBuildRoomURL(t *testing.T) {
}
}
}
func TestValidateGen(t *testing.T) {
RegisterDefaults()
tests := []struct {
name string
cfg Config
want error
}{
{
name: "valid wbstream",
cfg: Config{Carrier: "wbstream", DNSServer: "1.1.1.1:53", Amount: 3},
},
{
name: "valid jazz",
cfg: Config{Carrier: "jazz", DNSServer: "1.1.1.1:53", Amount: 1},
},
{
name: "missing carrier",
cfg: Config{DNSServer: "1.1.1.1:53", Amount: 1},
want: ErrCarrierRequired,
},
{
name: "unsupported carrier",
cfg: Config{Carrier: "unknown", DNSServer: "1.1.1.1:53", Amount: 1},
want: ErrUnsupportedCarrier,
},
{
name: "missing dns",
cfg: Config{Carrier: "wbstream", Amount: 1},
want: ErrDNSServerRequired,
},
{
name: "amount zero",
cfg: Config{Carrier: "wbstream", DNSServer: "1.1.1.1:53", Amount: 0},
want: ErrAmountRequired,
},
{
name: "amount negative",
cfg: Config{Carrier: "wbstream", DNSServer: "1.1.1.1:53", Amount: -1},
want: ErrAmountRequired,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateGen(tt.cfg)
if tt.want == nil {
if err != nil {
t.Fatalf("ValidateGen() error = %v", err)
}
return
}
if !errors.Is(err, tt.want) {
t.Fatalf("ValidateGen() error = %v, want %v", err, tt.want)
}
})
}
}
func TestGenUnsupportedCarrier(t *testing.T) {
RegisterDefaults()
cfg := Config{Carrier: "telemost", DNSServer: "1.1.1.1:53", Amount: 1}
err := Gen(context.Background(), cfg, func(string) {})
if !errors.Is(err, ErrUnsupportedCarrier) {
t.Fatalf("Gen(telemost) error = %v, want ErrUnsupportedCarrier", err)
}
}

View File

@@ -103,32 +103,15 @@ esac
echo "[*] Using transport: $TRANSPORT"
echo ""
if [ "$CARRIER" = "jazz" ]; then
echo "Jazz room options:"
echo " 1) Auto-generate new room (recommended)"
echo " 2) Use specific room ID (enter roomId:password)"
read -p "Enter choice [1-2, default: 1]: " JAZZ_CHOICE
GEN_ROOM=0
case "$JAZZ_CHOICE" in
2)
read -p "Enter Room ID (format: roomId:password): " ROOM_ID
if [ -z "$ROOM_ID" ]; then
echo "[X] Room ID cannot be empty"
exit 1
fi
;;
*)
ROOM_ID="any"
echo "[*] Will auto-generate Jazz room"
;;
esac
elif [ "$CARRIER" = "wbstream" ]; then
echo "WB Stream room options:"
if [ "$CARRIER" = "jazz" ] || [ "$CARRIER" = "wbstream" ]; then
echo "Room options:"
echo " 1) Auto-generate new room (recommended)"
echo " 2) Use specific room ID"
read -p "Enter choice [1-2, default: 1]: " WB_CHOICE
read -p "Enter choice [1-2, default: 1]: " ROOM_CHOICE
case "$WB_CHOICE" in
case "$ROOM_CHOICE" in
2)
read -p "Enter Room ID: " ROOM_ID
if [ -z "$ROOM_ID" ]; then
@@ -137,8 +120,9 @@ elif [ "$CARRIER" = "wbstream" ]; then
fi
;;
*)
ROOM_ID="any"
echo "[*] Will auto-generate WB Stream room"
GEN_ROOM=1
ROOM_ID=""
echo "[*] Will generate room before starting server"
;;
esac
else
@@ -294,6 +278,20 @@ if [ ! -f "$WORK_DIR/olcrtc" ]; then
exit 1
fi
if [ "$GEN_ROOM" = "1" ]; then
echo "[*] Generating room via -mode gen..."
ROOM_ID=$(podman run --rm \
-v $WORK_DIR:/app:Z \
-w /app \
$IMAGE_NAME \
./olcrtc -mode gen -carrier "$CARRIER" -dns "$DNS" -amount 1 -data data)
if [ -z "$ROOM_ID" ]; then
echo "[X] Room generation failed"
exit 1
fi
echo "[+] Generated room ID: $ROOM_ID"
fi
KEY_FILE="$HOME/.olcrtc_key"
if [ -f "$KEY_FILE" ]; then
@@ -323,47 +321,13 @@ podman run -d \
-link direct -transport "$TRANSPORT" -dns "$DNS" -data data \
"${EXTRA_ARGS[@]}" "${TRANSPORT_ARGS[@]}"
sleep 3
ACTUAL_ROOM_ID="$ROOM_ID"
if [ "$CARRIER" = "jazz" ] && [ "$ROOM_ID" = "any" ]; then
echo "[*] Waiting for Jazz room creation..."
sleep 2
LOGS=$(podman logs $CONTAINER_NAME 2>&1)
ACTUAL_ROOM_ID=$(echo "$LOGS" | grep -oP 'Jazz room created: \K[^\s]+' | head -1)
if [ -z "$ACTUAL_ROOM_ID" ]; then
echo "[!] WARNING: Could not extract Jazz room ID from logs"
echo "[*] Full logs:"
podman logs $CONTAINER_NAME
ACTUAL_ROOM_ID="(check logs above)"
else
echo "[+] Jazz room created: $ACTUAL_ROOM_ID"
fi
elif [ "$CARRIER" = "wbstream" ] && [ "$ROOM_ID" = "any" ]; then
echo "[*] Waiting for WB Stream room creation..."
sleep 2
LOGS=$(podman logs $CONTAINER_NAME 2>&1)
ACTUAL_ROOM_ID=$(echo "$LOGS" | grep -oP 'WB Stream room created: \K[^\s]+' | head -1)
if [ -z "$ACTUAL_ROOM_ID" ]; then
echo "[!] WARNING: Could not extract WB Stream room ID from logs"
echo "[*] Full logs:"
podman logs $CONTAINER_NAME
ACTUAL_ROOM_ID="(check logs above)"
else
echo "[+] WB Stream room created: $ACTUAL_ROOM_ID"
fi
fi
echo ""
echo "[+] Server started successfully!"
echo ""
echo "Container name: $CONTAINER_NAME"
echo "Carrier: $CARRIER"
echo "Transport: $TRANSPORT"
echo "Room ID: $ACTUAL_ROOM_ID"
echo "Room ID: $ROOM_ID"
echo "Client ID: $CLIENT_ID"
echo "Encryption key: $KEY"
@@ -379,7 +343,7 @@ echo "Stop server:"
echo " podman stop $CONTAINER_NAME"
echo ""
echo "Client command:"
echo -n " ./olcrtc -mode cnc -carrier \"$CARRIER\" -id \"$ACTUAL_ROOM_ID\" -client-id \"$CLIENT_ID\" -key \"$KEY\" \\"
echo -n " ./olcrtc -mode cnc -carrier \"$CARRIER\" -id \"$ROOM_ID\" -client-id \"$CLIENT_ID\" -key \"$KEY\" \\"
echo ""
echo -n " -link direct -transport \"$TRANSPORT\" -dns 1.1.1.1:53 -data data \\"
echo ""