feat: Add TURN server support
All checks were successful
Queue Release Build / prepare (push) Successful in 15s
Deploy Web Apps / deploy (push) Successful in 5m35s
Queue Release Build / build-linux (push) Successful in 24m45s
Queue Release Build / build-windows (push) Successful in 13m52s
Queue Release Build / finalize (push) Successful in 23s

This commit is contained in:
2026-04-18 21:27:04 +02:00
parent 167c45ba8d
commit 44588e8789
60 changed files with 2404 additions and 365 deletions

View File

@@ -0,0 +1,122 @@
import {
Injectable,
signal,
computed,
type Signal
} from '@angular/core';
import { STORAGE_KEY_ICE_SERVERS } from '../../core/constants';
import { ICE_SERVERS } from './realtime.constants';
export interface IceServerEntry {
id: string;
type: 'stun' | 'turn';
urls: string;
username?: string;
credential?: string;
}
const DEFAULT_ENTRIES: IceServerEntry[] = ICE_SERVERS.map((server, index) => ({
id: `default-stun-${index}`,
type: 'stun' as const,
urls: Array.isArray(server.urls) ? server.urls[0] : server.urls
}));
@Injectable({ providedIn: 'root' })
export class IceServerSettingsService {
readonly entries: Signal<IceServerEntry[]>;
readonly rtcIceServers: Signal<RTCIceServer[]>;
private readonly _entries = signal<IceServerEntry[]>(this.load());
constructor() {
this.entries = this._entries.asReadonly();
this.rtcIceServers = computed<RTCIceServer[]>(() =>
this._entries().map((entry) => {
if (entry.type === 'turn') {
return {
urls: entry.urls,
username: entry.username ?? '',
credential: entry.credential ?? ''
};
}
return { urls: entry.urls };
})
);
}
addEntry(entry: Omit<IceServerEntry, 'id'>): void {
const id = `${entry.type}-${Date.now()}-${Math.random().toString(36)
.slice(2, 8)}`;
const updated = [...this._entries(), { ...entry, id }];
this._entries.set(updated);
this.save(updated);
}
removeEntry(id: string): void {
const updated = this._entries().filter((entry) => entry.id !== id);
this._entries.set(updated);
this.save(updated);
}
updateEntry(id: string, changes: Partial<Omit<IceServerEntry, 'id'>>): void {
const updated = this._entries().map((entry) =>
entry.id === id ? { ...entry, ...changes } : entry
);
this._entries.set(updated);
this.save(updated);
}
moveEntry(fromIndex: number, toIndex: number): void {
const entries = [...this._entries()];
if (fromIndex < 0 || fromIndex >= entries.length || toIndex < 0 || toIndex >= entries.length) {
return;
}
const [moved] = entries.splice(fromIndex, 1);
entries.splice(toIndex, 0, moved);
this._entries.set(entries);
this.save(entries);
}
restoreDefaults(): void {
this._entries.set([...DEFAULT_ENTRIES]);
this.save(DEFAULT_ENTRIES);
}
private load(): IceServerEntry[] {
try {
const raw = localStorage.getItem(STORAGE_KEY_ICE_SERVERS);
if (!raw) {
return [...DEFAULT_ENTRIES];
}
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed) || parsed.length === 0) {
return [...DEFAULT_ENTRIES];
}
return parsed.filter(
(entry: unknown): entry is IceServerEntry =>
typeof entry === 'object'
&& entry !== null
&& typeof (entry as IceServerEntry).id === 'string'
&& ((entry as IceServerEntry).type === 'stun' || (entry as IceServerEntry).type === 'turn')
&& typeof (entry as IceServerEntry).urls === 'string'
);
} catch {
return [...DEFAULT_ENTRIES];
}
}
private save(entries: IceServerEntry[]): void {
localStorage.setItem(STORAGE_KEY_ICE_SERVERS, JSON.stringify(entries));
}
}