"""SIP-Anruf Klasse - Wrapper um pjsua2.Call mit Qt-Signalweiterleitung.""" import pjsua2 as pj class MeinAnruf(pj.Call): """Repräsentiert einen einzelnen SIP-Anruf. Leitet PJSUA2-Callbacks an eine Callback-Funktion weiter, die dann Qt-Signals im Hauptthread auslöst. Audio-Verbindung wird von PJSUA2 automatisch über die Conference Bridge gemanagt (setCaptureDev/setPlaybackDev). Kein manuelles startTransmit nötig. """ def __init__(self, acc, call_id=pj.PJSUA_INVALID_ID, callback=None): pj.Call.__init__(self, acc, call_id) self._callback = callback self._ist_aktiv = False @property def ist_aktiv(self): """Gibt True zurück wenn der Anruf gerade verbunden ist.""" return self._ist_aktiv def onCallState(self, prm): """Wird bei jeder Änderung des Anrufstatus aufgerufen.""" ci = self.getInfo() zustand = ci.stateText if ci.state == pj.PJSIP_INV_STATE_CONFIRMED: self._ist_aktiv = True elif ci.state == pj.PJSIP_INV_STATE_DISCONNECTED: self._ist_aktiv = False if self._callback: self._callback("zustand", { "state": ci.state, "stateText": zustand, "remoteUri": ci.remoteUri, "lastReason": ci.lastReason, "lastStatusCode": ci.lastStatusCode, "connectDuration": ci.connectDuration.sec, }) def onCallMediaState(self, prm): """Audio-Verbindung bei aktivem Media sicherstellen. startTransmit() auf bereits verbundenen Ports ist ein No-Op, daher sicher bei mehrfachem Aufruf (183→200 Neuverhandlung). """ ci = self.getInfo() for i, mi in enumerate(ci.media): if mi.type == pj.PJMEDIA_TYPE_AUDIO and \ mi.status == pj.PJSUA_CALL_MEDIA_ACTIVE: try: call_audio = self.getAudioMedia(i) aud_mgr = pj.Endpoint.instance().audDevManager() # Mikrofon → Anruf aud_mgr.getCaptureDevMedia().startTransmit(call_audio) # Anruf → Lautsprecher call_audio.startTransmit( aud_mgr.getPlaybackDevMedia() ) except pj.Error: pass def onCallTransferRequest(self, prm): """Eingehende Weiterleitungs-Anfrage.""" if self._callback: self._callback("transfer_anfrage", {"ziel": prm.currentTarget}) def onDtmfDigit(self, prm): """DTMF-Ton empfangen.""" if self._callback: self._callback("dtmf_empfangen", {"digit": prm.digit}) def dtmf_senden(self, ziffern): """DTMF-Töne senden (z.B. für IVR-Menüs).""" try: prm = pj.CallSendDtmfParam() prm.digits = ziffern prm.method = pj.PJSUA_DTMF_METHOD_RFC2833 self.sendDtmf(prm) except pj.Error as e: if self._callback: self._callback("fehler", {"text": f"DTMF-Fehler: {e}"}) def halten(self): """Anruf auf Halten setzen.""" try: prm = pj.CallOpParam(True) self.setHold(prm) except pj.Error as e: if self._callback: self._callback("fehler", {"text": f"Hold-Fehler: {e}"}) def fortsetzen(self): """Gehaltenen Anruf fortsetzen.""" try: prm = pj.CallOpParam(True) prm.opt.flag = pj.PJSUA_CALL_UNHOLD prm.opt.audioCount = 1 prm.opt.videoCount = 0 self.reinvite(prm) except pj.Error as e: if self._callback: self._callback("fehler", {"text": f"Unhold-Fehler: {e}"}) def stummschalten(self, stumm): """Mikrofon stumm schalten / wieder aktivieren. Nutzt adjustTxLevel() statt stopTransmit() - zuverlässiger weil es die Conference-Bridge-Verbindung nicht unterbricht. """ try: aud_mgr = pj.Endpoint.instance().audDevManager() if stumm: # TX-Level auf 0 = Stille senden aud_mgr.getCaptureDevMedia().adjustTxLevel(0.0) else: # TX-Level auf 1 = normale Lautstärke aud_mgr.getCaptureDevMedia().adjustTxLevel(1.0) except pj.Error: pass def blind_transfer(self, ziel_uri): """Blinde Weiterleitung an eine andere Nummer.""" try: prm = pj.CallOpParam() self.xfer(ziel_uri, prm) except pj.Error as e: if self._callback: self._callback("fehler", {"text": f"Transfer-Fehler: {e}"}) def attended_transfer(self, anderer_anruf): """Vermittelte Weiterleitung (Attended Transfer).""" try: prm = pj.CallOpParam() self.xferReplaces(anderer_anruf, prm) except pj.Error as e: if self._callback: self._callback("fehler", {"text": f"Attended-Transfer-Fehler: {e}"}) def auflegen(self): """Anruf beenden.""" try: prm = pj.CallOpParam() prm.statusCode = pj.PJSIP_SC_DECLINE self.hangup(prm) except pj.Error: pass # Anruf war bereits beendet