sources/webrtc-diagnostics/src_front/src/webrtc/negotiate.ts

Текстовый файл из учебного комплекта, не свидетельство установленного runtime. Вернуться к учебнику · Исходный файл

   1 import type { AppConfig } from "../config/appConfig";
   2 
   3 export async function negotiate(
   4   pc: RTCPeerConnection,
   5   config: AppConfig,
   6   opts?: { sessionId?: string | null; signal?: AbortSignal; onProgress?: (message: string, status?: number) => void },
   7 ): Promise<string | null> {
   8   const offer = await pc.createOffer();
   9   await pc.setLocalDescription(offer);
  10 
  11   await waitIceGather(pc, config.webrtc.iceGatherTimeoutMs, opts?.signal);
  12   opts?.signal?.throwIfAborted();
  13 
  14   const local = pc.localDescription;
  15   if (!local) throw new Error("localDescription is missing");
  16 
  17   const body: Record<string, unknown> = { sdp: local.sdp, type: local.type };
  18   const sessionId = opts?.sessionId;
  19   if (typeof sessionId === "string" && sessionId) body.session_id = sessionId;
  20 
  21   opts?.onProgress?.("Sending SDP offer (body not logged)");
  22   const resp = await fetch(config.signaling.offerEndpoint, {
  23     method: "POST",
  24     headers: { "Content-Type": "application/json" },
  25     body: JSON.stringify(body),
  26     signal: opts?.signal,
  27   });
  28   opts?.onProgress?.(`Signaling HTTP response: ${resp.status}`, resp.status);
  29 
  30   if (!resp.ok) throw new Error(`Signaling server responded with ${resp.status}`);
  31 
  32   const answer = (await resp.json()) as any;
  33   const sdp = typeof answer?.sdp === "string" ? answer.sdp : null;
  34   const type = typeof answer?.type === "string" ? answer.type : null;
  35   if (!sdp || !type) throw new Error("Signaling answer is missing sdp/type");
  36   await pc.setRemoteDescription({ sdp, type });
  37 
  38   return typeof answer?.session_id === "string" ? answer.session_id : null;
  39 }
  40 
  41 function waitIceGather(pc: RTCPeerConnection, timeoutMs: number, signal?: AbortSignal): Promise<void> {
  42   signal?.throwIfAborted();
  43   if (pc.iceGatheringState === "complete") return Promise.resolve();
  44 
  45   return new Promise((resolve, reject) => {
  46     const cleanup = () => {
  47       window.clearTimeout(timer);
  48       pc.removeEventListener("icegatheringstatechange", onChange);
  49       signal?.removeEventListener("abort", onAbort);
  50     };
  51     const timer = window.setTimeout(() => { cleanup(); resolve(); }, timeoutMs);
  52 
  53     const onChange = () => {
  54       if (pc.iceGatheringState === "complete") {
  55         cleanup();
  56         resolve();
  57       }
  58     };
  59     const onAbort = () => { cleanup(); reject(signal?.reason ?? new DOMException("Cancelled", "AbortError")); };
  60 
  61     pc.addEventListener("icegatheringstatechange", onChange);
  62     signal?.addEventListener("abort", onAbort, { once: true });
  63   });
  64 }