TeaWeb/shared/js/connection/rtc/Negotiation.ts

314 lines
8.1 KiB
TypeScript
Raw Normal View History

import {SessionDescription} from "sdp-transform";
import * as sdpTransform from "sdp-transform";
import { tr, tra } from "tc-shared/i18n/localize";
export interface RTCNegotiationMediaMapping {
direction: "sendrecv" | "recvonly" | "sendonly" | "inactive",
ssrc: number
}
export interface RTCNegotiationIceConfig {
username: string,
password: string,
fingerprint: string,
fingerprint_type: string,
setup: "active" | "passive" | "actpass",
candidates: string[]
}
export interface RTCNegotiationExtension {
id: number;
uri: string;
media?: "audio" | "video";
direction?: "recvonly" | "sendonly";
config?: string;
}
export interface RTCNegotiationCodec {
payload: number,
name: string,
channels?: number,
rate?: number,
fmtp?: string,
feedback?: string[]
}
/** The offer send by the client to the server */
export interface RTCNegotiationOffer {
type: "initial-offer" | "negotiation-offer",
sessionId: number,
ssrcs: number[],
ssrc_types: number[],
ice: RTCNegotiationIceConfig,
/* Only present in initial response */
extension: RTCNegotiationExtension | undefined
}
/** The offer send by the server to the client */
export interface RTCNegotiationMessage {
type: "initial-offer" | "negotiation-offer" | "initial-answer" | "negotiation-answer",
sessionId: number,
sessionUsername: string,
ssrc: number[],
ssrc_flags: number[],
ice: RTCNegotiationIceConfig,
/* Only present in initial answer */
extension: RTCNegotiationExtension[] | undefined,
/* Only present in initial answer */
audio_codecs: RTCNegotiationCodec[] | undefined,
/* Only present in initial answer */
video_codecs: RTCNegotiationCodec[] | undefined
}
type RTCDirection = "sendonly" | "recvonly" | "sendrecv" | "inactive";
type SsrcFlags = {
type: "audio",
mode: RTCDirection
} | {
type: "video",
mode: RTCDirection
}
namespace SsrcFlags {
function parseRtcDirection(direction: number) : RTCDirection {
switch (direction) {
case 0: return "sendrecv";
case 1: return "sendonly";
case 2: return "recvonly";
case 3: return "inactive";
default: throw tra("invalid rtc direction type {}", direction);
}
}
export function parse(flags: number) : SsrcFlags {
switch (flags & 0x7) {
case 0:
return {
type: "audio",
mode: parseRtcDirection((flags >> 3) & 0x3)
};
case 1:
return {
type: "video",
mode: parseRtcDirection((flags >> 3) & 0x3)
};
default:
throw tr("invalid ssrc flags");
}
}
}
function generateSdp(session: RTCNegotiationMessage) {
const sdp = {} as SessionDescription;
sdp.version = 0;
sdp.origin = {
username: session.sessionUsername,
sessionId: session.sessionId,
ipVer: 4,
netType: "IN",
address: "127.0.0.1",
sessionVersion: 2
};
sdp.name = "-";
sdp.timing = { start: 0, stop: 0 };
sdp.groups = [{
type: "BUNDLE",
mids: [...session.ssrc].map((_, idx) => idx).join(" ")
}];
sdp.media = [];
const generateMedia = (ssrc: number, flags: SsrcFlags) => {
let formats: RTCNegotiationCodec[];
if(flags.type === "audio") {
formats = session.audio_codecs;
} else if(flags.type === "video") {
formats = session.video_codecs;
}
const index = sdp.media.push({
type: flags.type,
port: 9,
protocol: "UDP/TLS/RTP/SAVPF",
payloads: formats.map(e => e.payload).join(" "),
fmtp: [],
rtp: [],
rtcpFb: [],
ext: [],
ssrcs: [],
invalid: []
});
const tMedia = sdp.media[index - 1];
/* basic properties */
tMedia.mid = (index - 1).toString();
tMedia.direction = flags.mode;
tMedia.rtcpMux = "rtcp-mux";
/* ice */
tMedia.iceUfrag = session.ice.username;
tMedia.icePwd = session.ice.password;
tMedia.invalid.push({ value: "ice-options:trickle" });
tMedia.fingerprint = {
hash: session.ice.fingerprint,
type: session.ice.fingerprint_type,
};
tMedia.setup = session.ice.setup;
/* codecs */
for(const codec of formats) {
tMedia.rtp.push({
codec: codec.name,
payload: codec.payload,
rate: codec.rate,
encoding: codec.channels
});
for(const feedback of codec.feedback || []) {
tMedia.rtcpFb.push({
payload: codec.payload,
type: feedback
});
}
if(codec.fmtp) {
tMedia.fmtp.push({
payload: codec.payload,
config: codec.fmtp
});
}
}
/* extensions */
for(const extension of session.extension || []) {
if(extension.media && extension.media !== tMedia.type) {
continue;
}
tMedia.ext.push({
value: extension.id,
config: extension.config,
direction: extension.direction,
uri: extension.uri
});
}
/* ssrc */
tMedia.ssrcs.push({
id: ssrc >>> 0,
attribute: "cname",
value: (ssrc >>> 0).toString(16)
});
};
for(let index = 0; index < session.ssrc.length; index++) {
const flags = SsrcFlags.parse(session.ssrc_flags[index]);
generateMedia(session.ssrc[index], flags);
}
return sdpTransform.write(sdp);
}
export class RTCNegotiator {
private readonly peer: RTCPeerConnection;
public callbackData: (data: string) => void;
public callbackFailed: (reason: string) => void;
private sessionCodecs: RTCNegotiationCodec | undefined;
private sessionExtensions: RTCNegotiationExtension | undefined;
constructor(peer: RTCPeerConnection) {
this.peer = peer;
}
doInitialNegotiation() {
}
handleRemoteData(dataString: string) {
}
}
/* TODO: mozilla...THIS_IS_SDPARTA-82.0.3 (Needs to be parsed from the offer) */
/*
console.error("XYX\n%s",
generateSdp({
sessionId: "1234",
audio_media: [
{
direction: "sendrecv",
ssrc: 123885,
},
{
direction: "sendrecv",
ssrc: 123885,
},
{
direction: "sendrecv",
ssrc: 123885,
},
{
direction: "sendrecv",
ssrc: 123885,
}
],
video_media: [
{
direction: "sendrecv",
ssrc: 123885,
},
{
direction: "sendrecv",
ssrc: 123885,
},
{
direction: "sendrecv",
ssrc: 123885,
},
{
direction: "sendrecv",
ssrc: 123885,
}
],
audio_codecs: [{
payload: 88,
channels: 2,
feedback: ["nack"],
fmtp: "minptime=10;useinbandfec=1",
name: "opus",
rate: 48000
}],
video_codecs: [{
payload: 89,
name: "VP8",
feedback: ["nack", "nack pli"]
}],
extension: [{
id: 1,
uri: "urn:ietf:params:rtp-hdrext:ssrc-audio-level"
}],
ice: {
candidates: [],
fingerprint: "E6:C3:F3:17:71:11:4B:E5:1A:DD:EC:3C:AA:F2:BB:48:08:3B:A5:69:18:44:4A:97:59:62:BF:B4:43:F1:5D:00",
fingerprint_type: "sha-256",
password: "passwd",
username: "uname",
setup: "actpass"
}
}, "-")
);
throw "dummy load error";
*/