{"version":3,"file":"index.mjs","names":[],"sources":["../../src/call-session/index.ts"],"sourcesContent":["import { Buffer } from \"node:buffer\";\nimport dgram from \"node:dgram\";\nimport EventEmitter from \"node:events\";\nimport waitFor from \"wait-for-async\";\nimport { RtpHeader, RtpPacket, SrtpSession } from \"werift-rtp\";\nimport DTMF from \"../dtmf.js\";\nimport type Softphone from \"../index.js\";\nimport {\n  type InboundMessage,\n  RequestMessage,\n  ResponseMessage,\n} from \"../sip-message/index.js\";\nimport { branch, extractAddress, localKey, randomInt } from \"../utils.js\";\nimport Streamer from \"./streamer.js\";\n\ntype DtmfChar = (typeof DTMF.phoneChars)[number];\n\nconst isDtmfChar = (value: string): value is DtmfChar =>\n  (DTMF.phoneChars as readonly string[]).includes(value);\n\nabstract class CallSession extends EventEmitter {\n  public softphone: Softphone;\n  public sipMessage: InboundMessage;\n  public socket!: dgram.Socket;\n  public localPeer!: string;\n  public remotePeer!: string;\n  public remoteIP!: string;\n  public remotePort!: number;\n  public disposed = false;\n  public srtpSession!: SrtpSession;\n  public encoder: { encode: (pcm: Buffer) => Buffer };\n  public decoder: { decode: (audio: Buffer) => Buffer };\n  public sdp!: string;\n\n  // for audio streaming\n  public ssrc = randomInt();\n  public sequenceNumber = randomInt();\n  public timestamp = randomInt();\n\n  public constructor(softphone: Softphone, sipMessage: InboundMessage) {\n    super();\n    this.softphone = softphone;\n    this.encoder = softphone.codec.createEncoder();\n    this.decoder = softphone.codec.createDecoder();\n    this.sipMessage = sipMessage;\n    // inbound call from call queue, invite message may not have body\n    if (this.sipMessage.body.length > 0) {\n      this.remoteIP = this.sipMessage.body.match(/c=IN IP4 ([\\d.]+)/)![1];\n      this.remotePort = parseInt(\n        this.sipMessage.body.match(/m=audio (\\d+) /)![1],\n        10,\n      );\n    }\n  }\n\n  public static async createBoundSocket() {\n    const socket = dgram.createSocket(\"udp4\");\n    return await new Promise<{ socket: dgram.Socket; port: number }>(\n      (resolve, reject) => {\n        const onError = (error: Error) => {\n          socket.removeListener(\"listening\", onListening);\n          socket.close();\n          reject(error);\n        };\n        const onListening = () => {\n          socket.removeListener(\"error\", onError);\n          const address = socket.address();\n          resolve({ socket, port: address.port });\n        };\n        socket.once(\"error\", onError);\n        socket.once(\"listening\", onListening);\n        socket.bind(0);\n      },\n    );\n  }\n\n  public set remoteKey(key: string) {\n    const localKeyBuffer = Buffer.from(localKey, \"base64\");\n    const remoteKeyBuffer = Buffer.from(key, \"base64\");\n    this.srtpSession = new SrtpSession({\n      profile: 0x0001,\n      keys: {\n        localMasterKey: localKeyBuffer.subarray(0, 16),\n        localMasterSalt: localKeyBuffer.subarray(16, 30),\n        remoteMasterKey: remoteKeyBuffer.subarray(0, 16),\n        remoteMasterSalt: remoteKeyBuffer.subarray(16, 30),\n      },\n    });\n  }\n\n  public get callId() {\n    return this.sipMessage.getHeader(\"Call-ID\");\n  }\n\n  public send(data: string | Buffer) {\n    this.socket.send(data, this.remotePort, this.remoteIP);\n  }\n\n  public async hangup() {\n    const requestMessage = new RequestMessage(\n      `BYE sip:${this.softphone.sipInfo.domain} SIP/2.0`,\n      {\n        \"Call-ID\": this.callId,\n        From: this.localPeer,\n        To: this.remotePeer,\n        Via: `SIP/2.0/TLS ${this.softphone.fakeDomain};branch=${branch()}`,\n      },\n    );\n    await this.softphone.send(requestMessage);\n  }\n\n  public sendDTMF(char: DtmfChar) {\n    const payloads = DTMF.charToPayloads(char);\n    const timestamp = this.timestamp;\n    let first = true;\n    for (const payload of payloads) {\n      const rtpHeader = new RtpHeader({\n        version: 2,\n        padding: false,\n        paddingSize: 0,\n        extension: false,\n        marker: first,\n        payloadOffset: 12,\n        payloadType: 101,\n        sequenceNumber: this.sequenceNumber,\n        timestamp,\n        ssrc: this.ssrc,\n        csrcLength: 0,\n        csrc: [],\n        extensionProfile: 48862,\n        extensionLength: undefined,\n        extensions: [],\n      });\n      const rtpPacket = new RtpPacket(rtpHeader, payload);\n      this.send(this.srtpSession.encrypt(rtpPacket.payload, rtpPacket.header));\n      this.sequenceNumber = (this.sequenceNumber + 1) % 65536;\n      first = false;\n    }\n    this.timestamp += 800;\n  }\n\n  public async sendDTMFs(s: string, delay = 500) {\n    for (const c of s) {\n      if (!isDtmfChar(c)) {\n        throw new Error(`invalid phone char: ${c}`);\n      }\n      this.sendDTMF(c);\n      await waitFor({ interval: delay });\n    }\n  }\n\n  // buffer is the content of a audio file, it is supposed to be uncompressed PCM data\n  // The audio should be playable by command: play -t raw -b 16 -r 16000 -e signed-integer test.wav\n  public streamAudio(input: Buffer) {\n    const streamer = new Streamer(this, input);\n    streamer.start();\n    return streamer;\n  }\n\n  // send a single rtp packet\n  public sendPacket(rtpPacket: RtpPacket) {\n    if (this.disposed) {\n      return;\n    }\n    this.send(this.srtpSession.encrypt(rtpPacket.payload, rtpPacket.header));\n  }\n\n  protected startLocalServices() {\n    if (!this.socket) {\n      throw new Error(\n        \"RTP socket is not initialized; expected pre-bound socket from SDP setup\",\n      );\n    }\n    this.socket.on(\"message\", (message) => {\n      const rtpPacket = RtpPacket.deSerialize(\n        this.srtpSession.decrypt(message),\n      );\n      this.emit(\"rtpPacket\", rtpPacket);\n      if (rtpPacket.header.payloadType === 101) {\n        this.emit(\"dtmfPacket\", rtpPacket);\n        const char = DTMF.payloadToChar(rtpPacket.payload);\n        if (char) {\n          this.emit(\"dtmf\", char);\n        }\n      } else if (rtpPacket.header.payloadType === this.softphone.codec.id) {\n        if (\n          rtpPacket.payload.length === 4 &&\n          rtpPacket.payload[0] >= 0x00 &&\n          rtpPacket.payload[0] < 0x0c &&\n          rtpPacket.payload[1] === 0x8a &&\n          rtpPacket.payload[2] === 0x03 &&\n          rtpPacket.payload[3] === 0xc0\n        ) {\n          // special DTMF packet in audio format\n          // first byte 0x00 to 0x0c means DTMF 0 to 9, *, #\n          // we ignore it since DTMF is handled by `if (rtpPacket.header.payloadType === 101) {`\n          return; // ignore it\n        }\n        try {\n          rtpPacket.payload = this.decoder.decode(rtpPacket.payload);\n          this.emit(\"audioPacket\", rtpPacket);\n        } catch {\n          console.error(\"Audio packet decode failed\", rtpPacket);\n        }\n      }\n    });\n\n    // send a message to remote server so that it knows where to reply\n    this.send(\"hello\");\n\n    const byeHandler = (inboundMessage: InboundMessage) => {\n      if (inboundMessage.getHeader(\"Call-ID\") !== this.callId) {\n        return;\n      }\n      if (inboundMessage.headers.CSeq.endsWith(\" BYE\")) {\n        this.softphone.off(\"message\", byeHandler);\n        this.dispose();\n      }\n    };\n    this.softphone.on(\"message\", byeHandler);\n  }\n\n  protected dispose() {\n    this.disposed = true;\n    this.emit(\"disposed\");\n    this.removeAllListeners();\n    this.socket?.removeAllListeners();\n    this.socket?.close();\n  }\n\n  public async transfer(transferTo: string) {\n    const requestMessage = new RequestMessage(\n      `REFER sip:${this.softphone.sipInfo.username}@${this.softphone.sipInfo.outboundProxy};transport=tls SIP/2.0`,\n      {\n        Via: `SIP/2.0/TLS ${this.softphone.client.localAddress}:${this.softphone.client.localPort};rport;branch=${branch()};alias`,\n        \"Max-Forwards\": 70,\n        From: this.localPeer,\n        To: this.remotePeer,\n        Contact: `<sip:${this.softphone.sipInfo.username}@${this.softphone.client.localAddress}:${this.softphone.client.localPort};transport=TLS;ob>`,\n        \"Call-ID\": this.callId,\n        Event: \"refer\",\n        Expires: 600,\n        Supported: \"replaces, 100rel, timer, norefersub\",\n        Accept: \"message/sipfrag;version=2.0\",\n        \"Allow-Events\": \"presence, message-summary, refer\",\n        \"Refer-To\": `sip:${transferTo}@${this.softphone.sipInfo.domain}`,\n        \"Referred-By\": `<sip:${this.softphone.sipInfo.username}@${this.softphone.sipInfo.domain}>`,\n      },\n    );\n    await this.softphone.send(requestMessage);\n\n    return new Promise<void>((resolve) => {\n      const notifyHandler = (inboundMessage: InboundMessage) => {\n        if (!inboundMessage.subject.startsWith(\"NOTIFY \")) {\n          return;\n        }\n        const responseMessage = new ResponseMessage(inboundMessage, 200);\n        this.softphone.send(responseMessage);\n        if (inboundMessage.body.trim() === \"SIP/2.0 200 OK\") {\n          this.softphone.off(\"message\", notifyHandler);\n          resolve();\n        }\n      };\n      this.softphone.on(\"message\", notifyHandler);\n    });\n  }\n\n  public async toggleReceive(toReceive: boolean) {\n    let newSDP = this.sdp;\n    if (!toReceive) {\n      newSDP = newSDP.replace(/a=sendrecv/, \"a=sendonly\");\n    }\n    const requestMessage = new RequestMessage(\n      `INVITE ${extractAddress(this.remotePeer)} SIP/2.0`,\n      {\n        \"Call-Id\": this.callId,\n        From: this.localPeer,\n        To: this.remotePeer,\n        Via: `SIP/2.0/TLS ${this.softphone.client.localAddress}:${this.softphone.client.localPort};rport;branch=${branch()};alias`,\n        \"Content-Type\": \"application/sdp\",\n        Contact: ` <sip:${this.softphone.sipInfo.username}@${this.softphone.client.localAddress}:${this.softphone.client.localPort};transport=TLS;ob>`,\n      },\n      newSDP,\n    );\n    const replyMessage = await this.softphone.send(requestMessage, true);\n    const ackMessage = new RequestMessage(\n      `ACK ${extractAddress(this.remotePeer)} SIP/2.0`,\n      {\n        \"Call-Id\": this.callId,\n        From: this.localPeer,\n        To: this.remotePeer,\n        Via: replyMessage.headers.Via,\n        CSeq: replyMessage.headers.CSeq.replace(\" INVITE\", \" ACK\"),\n      },\n    );\n    await this.softphone.send(ackMessage);\n  }\n\n  public async hold() {\n    return this.toggleReceive(false);\n  }\n\n  public async unhold() {\n    return this.toggleReceive(true);\n  }\n}\n\nexport default CallSession;\n"],"mappings":"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"}