
import sys

from errno import EADDRINUSE


# classes

cdef class RTPTransport:
    def __cinit__(self, *args, **kwargs):
        cdef int status
        cdef pj_pool_t *pool
        cdef bytes pool_name
        cdef char* c_pool_name
        cdef PJSIPUA ua

        ua = _get_ua()
        pool_name = b"RTPTransport_%d" % id(self)

        self.weakref = weakref.ref(self)
        Py_INCREF(self.weakref)

        self._af = pj_AF_INET()

        status = pj_mutex_create_recursive(ua._pjsip_endpoint._pool, "rtp_transport_lock", &self._lock)
        if status != 0:
            raise PJSIPError("failed to create lock", status)

        pool = ua.create_memory_pool(pool_name, 4096, 4096)
        self._pool = pool
        self._zrtp_transport = NULL
        self.state = "NULL"

    def __init__(self, encryption=None, use_ice=False, ice_stun_address=None, ice_stun_port=PJ_STUN_PORT):
        cdef PJSIPUA ua = _get_ua()

        if self.state != "NULL":
            raise SIPCoreError("RTPTransport.__init__() was already called")
        self._rtp_valid_pair = None
        self._encryption = encryption
        self.use_ice = use_ice
        self.ice_stun_address = ice_stun_address
        self.ice_stun_port = ice_stun_port

    def __dealloc__(self):
        cdef PJSIPUA ua
        cdef pjmedia_transport *transport
        cdef Timer timer

        try:
            ua = _get_ua()
        except:
            return

        transport = self._obj
        if transport != NULL:
            transport.user_data = NULL
            if self._wrapped_transport != NULL:
                self._wrapped_transport.user_data = NULL
            if self._zrtp_transport != NULL:
                # Middle layer of the opportunistic stacked chain. The
                # outermost SRTP transport will cascade-close it via
                # close_member_tp; we only need to NULL out user_data so
                # late ZRTP callbacks don't try to resolve a dead weakref.
                self._zrtp_transport.user_data = NULL
            # Mark the wrapper as invalid BEFORE freeing the underlying
            # pjmedia transport. The guard in _get_info() checks this
            # state under the lock and bails out instead of walking the
            # freed SRTP streams[] table. Without this ordering a
            # concurrent greenlet calling _get_info() between
            # pjmedia_transport_close() and the `self._obj = NULL`
            # assignment below would still see a non-NULL self._obj
            # pointing at freed memory.
            self._srtp_streams_dangling = 1
            self.state = "INVALID"
            with nogil:
                pjmedia_transport_media_stop(transport)
                pjmedia_transport_close(transport)
            self._obj = NULL
            self._wrapped_transport = NULL
            self._zrtp_transport = NULL
        ua.release_memory_pool(self._pool)
        self._pool = NULL
        if self._lock != NULL:
            pj_mutex_destroy(self._lock)
        timer = Timer()
        try:
            timer.schedule(60, deallocate_weakref, self.weakref)
        except SIPCoreError:
            pass

    cdef PJSIPUA _check_ua(self):
        cdef PJSIPUA ua
        try:
            ua = _get_ua()
            return ua
        except:
            self.state = "INVALID"
            self._obj = NULL
            self._wrapped_transport = NULL
            self._pool = NULL
            return None

    cdef void _get_info(self, pjmedia_transport_info *info):
        cdef int status
        cdef pjmedia_transport *transport
        cdef pj_mutex_t *lock = self._lock

        # Always zero the info struct first. pjmedia_transport_get_info()
        # zeroes internally on each entry too, so doing it here is
        # idempotent — but it also means the early-exit guards below
        # leave the caller with a deterministic empty sock_info instead
        # of whatever stack garbage happened to live in `info`.
        with nogil:
            pjmedia_transport_info_init(info)

        # Defensive guard against use-after-free in pjmedia's SRTP
        # transport wrapper.
        #
        # Observed crash: EXC_BAD_ACCESS at a poisoned heap address
        # inside srtp_get_stream_roc(), called from
        # pjmedia_transport_srtp::transport_get_info, called from the
        # pjmedia_transport_get_info() invocation below. The trigger
        # is AudioTransport.__init__ being constructed against an
        # RTPTransport whose SRTP context has been torn down on a
        # parallel greenlet by a hang-up / re-INVITE path: the SRTP
        # wrapper still holds a stale srtp_streams[] pointer at that
        # point and dereferencing one of those streams crashes.
        #
        # The defensive layers are:
        #   1. Refuse to enter if self._obj is NULL or the wrapper's
        #      state says the transport is gone. ``_check_ua`` may
        #      have nulled the obj already; ``__dealloc__`` does too.
        #   2. Take the wrapper's own recursive lock so we serialise
        #      against any concurrent state mutation on a different
        #      greenlet (set_LOCAL / set_REMOTE / set_INIT). The lock
        #      is RECURSIVE (pj_mutex_create_recursive at __cinit__)
        #      so callers that already hold it stay safe.
        #   3. Re-check under the lock — between the first check and
        #      the lock acquire another greenlet may have closed the
        #      transport.
        if self._obj == NULL or self.state in ("NULL", "INVALID"):
            raise SIPCoreError("RTPTransport._get_info called on a closed transport (state=%s)" % self.state)
        if self._srtp_streams_dangling:
            raise SIPCoreError("RTPTransport._get_info called after media_stop but before media_start "
                               "(SRTP streams[] is dangling — would crash in srtp_get_stream_roc); state=%s" % self.state)

        if lock != NULL:
            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire RTPTransport lock", status)
        try:
            transport = self._obj
            if transport == NULL or self.state in ("NULL", "INVALID"):
                raise SIPCoreError("RTPTransport._get_info called on a closed transport (state=%s)" % self.state)
            if self._srtp_streams_dangling:
                # Re-check under the lock: set_INIT() may have flipped
                # this on a different greenlet while we were waiting
                # for the lock acquire.
                raise SIPCoreError("RTPTransport._get_info called after media_stop but before media_start "
                                   "(SRTP streams[] is dangling); state=%s" % self.state)

            with nogil:
                # Zero again under the lock; cheap, and removes any
                # window where another thread observed the partially-
                # filled info from the first zero.
                pjmedia_transport_info_init(info)
                status = pjmedia_transport_get_info(transport, info)
            if status != 0:
                raise PJSIPError("Could not get transport info", status)
        finally:
            if lock != NULL:
                with nogil:
                    pj_mutex_unlock(lock)

    property local_rtp_port:

        def __get__(self):
            cdef int status
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_transport_info info
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return None

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                    return None
                self._get_info(&info)
                if pj_sockaddr_has_addr(&info.sock_info.rtp_addr_name):
                    return pj_sockaddr_get_port(&info.sock_info.rtp_addr_name)
                else:
                    return None
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    property local_rtp_address:

        def __get__(self):
            cdef char buf[PJ_INET6_ADDRSTRLEN]
            cdef int status
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_transport_info info
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return None

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                    return None
                self._get_info(&info)
                if pj_sockaddr_has_addr(&info.sock_info.rtp_addr_name):
                    return pj_sockaddr_print(&info.sock_info.rtp_addr_name, buf, PJ_INET6_ADDRSTRLEN, 0)
                else:
                    return None
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    property local_rtp_candidate:

        def __get__(self):
            cdef int status
            cdef pj_mutex_t *lock = self._lock
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return None

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self._rtp_valid_pair:
                    return self._rtp_valid_pair.local_candidate
                return None
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    property remote_rtp_port:

        def __get__(self):
            cdef int status
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_transport_info info
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return None

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                    return None
                if self._ice_active() and self._rtp_valid_pair:
                    return self._rtp_valid_pair.remote_candidate.port
                self._get_info(&info)
                if pj_sockaddr_has_addr(&info.src_rtp_name):
                    return pj_sockaddr_get_port(&info.src_rtp_name)
                else:
                    return None
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    property remote_rtp_address:

        def __get__(self):
            cdef char buf[PJ_INET6_ADDRSTRLEN]
            cdef int status
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_transport_info info
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return None

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                    return None
                if self._ice_active() and self._rtp_valid_pair:
                    return self._rtp_valid_pair.remote_candidate.address
                self._get_info(&info)
                if pj_sockaddr_has_addr(&info.src_rtp_name):
                    return pj_sockaddr_print(&info.src_rtp_name, buf, PJ_INET6_ADDRSTRLEN, 0)
                else:
                    return None
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    property remote_rtp_candidate:

        def __get__(self):
            cdef int status
            cdef pj_mutex_t *lock = self._lock
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return None

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self._rtp_valid_pair:
                    return self._rtp_valid_pair.remote_candidate
                return None
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    property srtp_active:

        def __get__(self):
            cdef int status
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_srtp_info *srtp_info
            cdef pjmedia_transport_info info
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return False

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                    return False
                self._get_info(&info)
                srtp_info = <pjmedia_srtp_info *> pjmedia_transport_info_get_spc_info(&info, PJMEDIA_TRANSPORT_TYPE_SRTP)
                if srtp_info != NULL:
                    return bool(srtp_info.active)
                return False
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    property srtp_cipher:

        def __get__(self):
            cdef int status
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_srtp_info *srtp_info
            cdef pjmedia_transport_info info
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return None

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                    return None
                self._get_info(&info)
                srtp_info = <pjmedia_srtp_info *> pjmedia_transport_info_get_spc_info(&info, PJMEDIA_TRANSPORT_TYPE_SRTP)
                if srtp_info == NULL or not bool(srtp_info.active):
                    return None
                return _pj_str_to_bytes(srtp_info.tx_policy.name)
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    property zrtp_active:

        def __get__(self):
            cdef int status
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_zrtp_info *zrtp_info
            cdef pjmedia_transport_info info
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return False

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                    return False
                self._get_info(&info)
                zrtp_info = <pjmedia_zrtp_info *> pjmedia_transport_info_get_spc_info(&info, PJMEDIA_TRANSPORT_TYPE_ZRTP)
                if zrtp_info != NULL:
                    return bool(zrtp_info.active)
                return False
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    cdef int _ice_active(self):
        # this function needs to be called with the lock held
        cdef pjmedia_transport_info info
        cdef pjmedia_ice_transport_info *ice_info

        if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
            return 0

        self._get_info(&info)
        ice_info = <pjmedia_ice_transport_info *> pjmedia_transport_info_get_spc_info(&info, PJMEDIA_TRANSPORT_TYPE_ICE)
        if ice_info != NULL and ice_info.sess_state == PJ_ICE_STRANS_STATE_RUNNING:
            return 1
        return 0

    property ice_active:

        def __get__(self):
            cdef int status
            cdef pj_mutex_t *lock = self._lock
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return False

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                return bool(self._ice_active())
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    cdef int _init_local_sdp(self, BaseSDPSession local_sdp, BaseSDPSession remote_sdp, int sdp_index):
        cdef int status
        cdef pj_pool_t *pool
        cdef pjmedia_sdp_session *pj_local_sdp
        cdef pjmedia_sdp_session *pj_remote_sdp
        cdef pjmedia_transport *transport

        pool = self._pool
        transport = self._obj
        pj_local_sdp = local_sdp.get_sdp_session()
        if remote_sdp is not None:
            pj_remote_sdp = remote_sdp.get_sdp_session()
        else:
            pj_remote_sdp = NULL
        if sdp_index < 0:
            raise ValueError("sdp_index argument cannot be negative")
        if sdp_index >= <int>pj_local_sdp.media_count:
            raise ValueError("sdp_index argument out of range")
        with nogil:
            status = pjmedia_transport_media_create(transport, pool, 0, pj_remote_sdp, sdp_index)
        if status != 0:
            raise PJSIPError("Could not create media transport", status)
        return 0

    def set_LOCAL(self, SDPSession local_sdp, int sdp_index):
        cdef int status
        cdef pj_mutex_t *lock = self._lock

        _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            if local_sdp is None:
                raise SIPCoreError("local_sdp argument cannot be None")
            if self.state == "LOCAL":
                return
            if self.state != "INIT":
                raise SIPCoreError('set_LOCAL can only be called in the "INIT" state, current state is "%s"' % self.state)
            self._init_local_sdp(local_sdp, None, sdp_index)
            self.state = "LOCAL"
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def set_REMOTE(self, BaseSDPSession local_sdp, BaseSDPSession remote_sdp, int sdp_index):
        cdef int status
        cdef pj_mutex_t *lock = self._lock

        _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            if None in [local_sdp, remote_sdp]:
                raise SIPCoreError("SDP arguments cannot be None")
            if self.state == "REMOTE":
                return
            if self.state != "INIT":
                raise SIPCoreError('set_REMOTE can only be called in the "INIT" state, current state is "%s"' % self.state)
            self._init_local_sdp(local_sdp, remote_sdp, sdp_index)
            self.state = "REMOTE"
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def set_ESTABLISHED(self, BaseSDPSession local_sdp, BaseSDPSession remote_sdp, int sdp_index):
        cdef int status
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_sdp_session *pj_local_sdp
        cdef pjmedia_sdp_session *pj_remote_sdp
        cdef pjmedia_transport *transport = self._obj

        _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            transport = self._obj

            if None in [local_sdp, remote_sdp]:
                raise SIPCoreError("SDP arguments cannot be None")
            pj_local_sdp = local_sdp.get_sdp_session()
            pj_remote_sdp = remote_sdp.get_sdp_session()
            if self.state == "ESTABLISHED":
                return
            if self.state not in ["LOCAL", "REMOTE"]:
                raise SIPCoreError('set_ESTABLISHED can only be called in the "INIT" and "LOCAL" states, ' +
                                   'current state is "%s"' % self.state)
            with nogil:
                status = pjmedia_transport_media_start(transport, self._pool, pj_local_sdp, pj_remote_sdp, sdp_index)
            if status != 0:
                raise PJSIPError("Could not start media transport", status)
            # SRTP streams[] has just been re-allocated inside
            # pjmedia_transport_srtp::media_start. Walking it via
            # transport_get_info is safe again until the next media_stop.
            self._srtp_streams_dangling = 0
            self.state = "ESTABLISHED"
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def rebind_remote_peer(self, BaseSDPSession remote_sdp, int sdp_index):
        """Update the underlying pjmedia transport's remote RTP/RTCP peer
        addresses in place from a new remote SDP, without going through
        media_stop / media_start. SRTP keying, ROC counters and any Sylk
        AEAD wrapper state are preserved.

        Used by AudioStream.update() in sipsimple/streams/rtp/audio.py
        when an incoming re-INVITE renumbers the peer's media port.
        Without this hook the old codepath teared down the AudioTransport
        and rebuilt it, which failed with "Could not generate base SDP
        (PJ_EAFNOTSUP)" because the new AudioTransport's _get_info read
        sock_info from an SRTP wrapper whose member transport had just
        been stopped.

        The C-level rebind is implemented in pjmedia/include/pjmedia/
        transport.h:pjmedia_transport_rebind_remote_peer, documented by
        deps/patches/27_pjmedia_rebind_remote_peer.patch. It is just a
        second pjmedia_transport_attach2() with NULL callbacks; SRTP's
        attach2 explicitly skips callback rebinding in that case and
        the new addresses propagate down to the UDP layer.
        """
        cdef int status
        cdef int af
        cdef int port
        cdef int rtcp_port = 0
        cdef bytes addr_bytes
        cdef pj_str_t addr_str
        cdef pj_sockaddr rem_rtp
        cdef pj_sockaddr rem_rtcp
        cdef pj_sockaddr *rem_rtcp_ptr
        cdef pjmedia_transport *transport
        cdef pj_mutex_t *lock = self._lock
        cdef unsigned int addr_len

        _get_ua()

        if remote_sdp is None:
            raise SIPCoreError("remote_sdp argument cannot be None")
        if sdp_index < 0 or sdp_index >= len(remote_sdp.media):
            raise SIPCoreError("sdp_index out of range")

        media = remote_sdp.media[sdp_index]
        connection = media.connection or remote_sdp.connection
        if connection is None:
            raise SIPCoreError("remote SDP has no connection address")

        addr_bytes = connection.address
        if b':' in addr_bytes:
            af = pj_AF_INET6()
        else:
            af = pj_AF_INET()
        _str_to_pj_str(addr_bytes, &addr_str)
        port = media.port

        with nogil:
            status = pj_sockaddr_init(af, &rem_rtp, &addr_str, port)
        if status != 0:
            raise PJSIPError("Could not parse remote RTP address", status)
        addr_len = pj_sockaddr_get_len(&rem_rtp)

        # If the peer included a=rtcp:<port>, honour it; otherwise the
        # C helper derives port+1.
        rem_rtcp_ptr = NULL
        for attr in media.attributes:
            if attr.name == b'rtcp':
                try:
                    parts = attr.value.split()
                    rtcp_port = int(parts[0])
                    if len(parts) >= 4:
                        # "<port> IN IP4 <addr>" form
                        rtcp_addr_bytes = parts[3]
                    else:
                        rtcp_addr_bytes = addr_bytes
                    if b':' in rtcp_addr_bytes:
                        af = pj_AF_INET6()
                    else:
                        af = pj_AF_INET()
                    _str_to_pj_str(rtcp_addr_bytes, &addr_str)
                    with nogil:
                        status = pj_sockaddr_init(af, &rem_rtcp, &addr_str, rtcp_port)
                    if status == 0:
                        rem_rtcp_ptr = &rem_rtcp
                except (ValueError, IndexError):
                    pass
                break

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire RTPTransport lock", status)
        try:
            transport = self._obj
            if transport == NULL or self.state in ("NULL", "INVALID"):
                raise SIPCoreError("RTPTransport.rebind_remote_peer called on a closed transport (state=%s)" % self.state)
            with nogil:
                status = pjmedia_transport_rebind_remote_peer(transport, &rem_rtp, rem_rtcp_ptr, addr_len)
            if status != 0:
                raise PJSIPError("Could not rebind remote peer", status)
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def set_aead_keys(self, bytes send_key, bytes send_salt,
                            bytes recv_key, bytes recv_salt,
                            int key_id=1, int video_prefix=0):
        """Activate the Sylk AES-128-GCM transport adapter for this RTP transport.

        Called after the in-dialog ZRTP handshake completes and Python has
        derived per-direction keys + salts via HKDF. The adapter (installed
        at set_INIT() time, sitting between the stream and the SRTP layer)
        switches from passthrough to active mode: outgoing RTP payload is
        AES-128-GCM encrypted, incoming RTP payload is decrypted + tag-
        verified. Permissive decrypt — payload that doesn't look like ours
        passes through unchanged so audio survives a mid-call rekey window.

        Wire format:
          [RTP header][video_prefix bytes plaintext][1B v|keyId]
          [4B counter_be][ciphertext][16B tag]
        Matches sylk-mobile's MediaEncryptorJni.cpp byte-for-byte.

        send_key / recv_key: 16 bytes (AES-128).
        send_salt / recv_salt: 8 bytes (prepended to counter to form 12-byte GCM IV).
        key_id: 0..15 (low 4 bits of header byte; peer must agree).
        video_prefix: codec-metadata bytes left UNENCRYPTED at start of payload.
                      Audio = 0. Video: VP8/VP9 = 3, H264 = 2, AV1 = 1.
                      Must match what the peer's decryptor uses for the same
                      negotiated codec, or every frame fails the tag check
                      and falls through to permissive passthrough.
        """
        cdef int status
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_transport *tp
        cdef unsigned char *p_send_key
        cdef unsigned char *p_send_salt
        cdef unsigned char *p_recv_key
        cdef unsigned char *p_recv_salt

        if len(send_key) != 16 or len(recv_key) != 16:
            raise ValueError("send_key and recv_key must each be 16 bytes")
        if len(send_salt) != 8 or len(recv_salt) != 8:
            raise ValueError("send_salt and recv_salt must each be 8 bytes")
        if key_id < 0 or key_id > 15:
            raise ValueError("key_id must be in [0, 15]")
        if video_prefix < 0 or video_prefix > 32:
            raise ValueError("video_prefix must be in [0, 32]")

        _get_ua()
        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            tp = self._obj
            if tp == NULL:
                raise SIPCoreError("RTPTransport has no underlying media transport")
            p_send_key  = <unsigned char *> send_key
            p_send_salt = <unsigned char *> send_salt
            p_recv_key  = <unsigned char *> recv_key
            p_recv_salt = <unsigned char *> recv_salt
            with nogil:
                status = sylk_aead_transport_set_keys(tp, p_send_key, p_send_salt,
                                                         p_recv_key, p_recv_salt,
                                                         <unsigned char> key_id,
                                                         <unsigned char> video_prefix)
            if status != 0:
                raise PJSIPError("Could not install Sylk AEAD keys", status)
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def get_aead_stats(self):
        """Return (encrypted_frames, decrypted_frames, passthrough_frames).

        - encrypted_frames   : RTP packets WE encrypted on the outbound path.
        - decrypted_frames   : inbound RTP packets whose AES-GCM tag verified
                               (peer is actually emitting our ciphertext).
        - passthrough_frames : inbound packets the permissive decryptor
                               bypassed (peer not encrypting, header
                               mismatch, or tag failure).

        Returns (0, 0, 0) if the adapter isn't installed (transport already
        torn down, or non-SDES encryption).
        """
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_transport *tp
        cdef unsigned long long enc = 0
        cdef unsigned long long dec = 0
        cdef unsigned long long passthrough = 0

        _get_ua()
        with nogil:
            pj_mutex_lock(lock)
        try:
            tp = self._obj
            if tp != NULL:
                with nogil:
                    sylk_aead_transport_get_stats(tp, &enc, &dec, &passthrough)
        finally:
            with nogil:
                pj_mutex_unlock(lock)
        return int(enc), int(dec), int(passthrough)

    def set_INIT(self):
        global _ice_cb
        cdef int af
        cdef int status
        cdef int port
        cdef pj_caching_pool *caching_pool
        cdef pj_ice_strans_cfg ice_cfg
        cdef pj_ice_strans *ice_st
        cdef pj_ice_strans_state ice_state
        cdef pj_mutex_t *lock = self._lock
        cdef pj_str_t local_ip
        cdef pj_str_t *local_ip_address
        cdef pjmedia_endpt *media_endpoint
        cdef pjmedia_srtp_setting srtp_setting
        cdef pjmedia_transport **transport_address
        cdef pjmedia_transport *wrapped_transport
        cdef pjmedia_transport *srtp_transport
        cdef pjsip_endpoint *sip_endpoint
        cdef bytes zid_file
        cdef char *c_zid_file
        cdef PJSIPUA ua

        ua = _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            af = self._af
            caching_pool = &ua._caching_pool._obj
            media_endpoint = ua._pjmedia_endpoint._obj
            sip_endpoint = ua._pjsip_endpoint._obj
            transport_address = &self._obj

            if self.state == "INIT":
                return
            if self.state in ["LOCAL", "ESTABLISHED"]:
                # CRITICAL ORDERING: raise the "streams are dangling"
                # flag BEFORE calling pjmedia_transport_media_stop().
                # The stop call frees the SRTP wrapper's streams[] but
                # leaves the table pointer non-NULL inside pjmedia, so
                # any subsequent transport_get_info() — most notably
                # the one in AudioTransport.__init__ on a fresh
                # re-INVITE — would walk freed memory and crash inside
                # srtp_get_stream_roc. _get_info() checks this flag
                # under the lock and bails out. Cleared on the next
                # successful media_start in set_ESTABLISHED.
                self._srtp_streams_dangling = 1
                with nogil:
                    status = pjmedia_transport_media_stop(transport_address[0])
                if status != 0:
                    raise PJSIPError("Could not stop media transport", status)
                self.state = "INIT"
            elif self.state == "NULL":
                if ua.ip_address is None:
                    local_ip_address = NULL
                else:
                    _str_to_pj_str(ua.ip_address, &local_ip)
                    local_ip_address = &local_ip
                if self.use_ice:
                    with nogil:
                        pj_ice_strans_cfg_default(&ice_cfg)
                    ice_cfg.af = self._af
                    with nogil:
                        pj_stun_config_init(&ice_cfg.stun_cfg, &caching_pool.factory, 0,
                                            pjmedia_endpt_get_ioqueue(media_endpoint),
                                            pjsip_endpt_get_timer_heap(sip_endpoint))
                    if self.ice_stun_address is not None:
                        _str_to_pj_str(self.ice_stun_address, &ice_cfg.stun.server)
                        ice_cfg.stun.port = self.ice_stun_port
                    # IIRC we can't choose the port for ICE
                    with nogil:
                        status = pj_sockaddr_init(ice_cfg.af, &ice_cfg.stun.cfg.bound_addr, local_ip_address, 0)
                    if status != 0:
                        raise PJSIPError("Could not init ICE bound address", status)
                    with nogil:
                        status = pjmedia_ice_create2(media_endpoint, NULL, 2, &ice_cfg, &_ice_cb, 0, transport_address)
                    if status != 0:
                        raise PJSIPError("Could not create ICE media transport", status)
                else:
                    status = PJ_EBUG
                    for i in xrange(ua._rtp_port_index, ua._rtp_port_index + ua._rtp_port_usable_count, 2):
                        port = ua._rtp_port_start + i % ua._rtp_port_usable_count
                        with nogil:
                            status = pjmedia_transport_udp_create3(media_endpoint, af, NULL, local_ip_address,
                                                                   port, 0, transport_address)
                        if status != PJ_ERRNO_START_SYS + EADDRINUSE:
                            ua._rtp_port_index = (i + 2) % ua._rtp_port_usable_count
                            break
                    if status != 0:
                        raise PJSIPError("Could not create UDP/RTP media transport", status)
                self._obj.user_data = <void *> self.weakref
                if self._encryption is not None:
                    wrapped_transport = self._wrapped_transport = self._obj
                    self._obj = NULL
                    if self._encryption.startswith('sdes'):
                        with nogil:
                            pjmedia_srtp_setting_default(&srtp_setting)
                        if self._encryption == 'sdes_mandatory':
                            srtp_setting.use = PJMEDIA_SRTP_MANDATORY
                        # Offer only AES_CM_128_HMAC_SHA1_80. This is the
                        # SDES baseline (RFC 4568) and works around peers
                        # that mishandle multi-crypto offers (e.g. Janus's
                        # sofia-sip-based SIP plugin before PR-2727).
                        srtp_setting.crypto_count = 1
                        _str_to_pj_str(b"AES_CM_128_HMAC_SHA1_80", &srtp_setting.crypto[0].name)
                        srtp_setting.crypto[0].key.ptr = NULL
                        srtp_setting.crypto[0].key.slen = 0
                        srtp_setting.crypto[0].flags = 0
                        with nogil:
                            status = pjmedia_transport_srtp_create(media_endpoint, wrapped_transport, &srtp_setting, transport_address)
                        if status != 0:
                            with nogil:
                                pjmedia_transport_close(wrapped_transport)
                            self._wrapped_transport = NULL
                            raise PJSIPError("Could not create SRTP media transport", status)
                        # Always wrap the SRTP transport with our Sylk AEAD
                        # adapter. Starts in passthrough mode — RTP packets
                        # go through unchanged until set_aead_keys() is
                        # called (typically after the in-dialog ZRTP
                        # handshake completes). Once active, the adapter
                        # adds AES-128-GCM on the RTP payload BEFORE handing
                        # to SRTP. Final wire stack: payload → AES-GCM
                        # adapter → SRTP → UDP. Mirrors what react-native-
                        # webrtc does on Sylk Mobile with its FrameEncryptor
                        # between codec and SRTP.
                        srtp_transport = self._obj
                        self._obj = NULL
                        with nogil:
                            status = sylk_aead_transport_create(media_endpoint, NULL, srtp_transport, 1, transport_address)
                        if status != 0:
                            with nogil:
                                pjmedia_transport_close(srtp_transport)
                            self._wrapped_transport = NULL
                            raise PJSIPError("Could not create Sylk AEAD adapter", status)
                    elif self._encryption == 'zrtp':
                        with nogil:
                            status = pjmedia_transport_zrtp_create(media_endpoint, pjsip_endpt_get_timer_heap(sip_endpoint), wrapped_transport, transport_address, 1)
                        if status == 0:
                            zid_file = ua.zrtp_cache.encode(sys.getfilesystemencoding())
                            c_zid_file = zid_file
                            with nogil:
                                # Auto-enable is deactivated
                                status = pjmedia_transport_zrtp_initialize(self._obj, c_zid_file, 0, &_zrtp_cb)
                        if status != 0:
                            with nogil:
                                pjmedia_transport_close(wrapped_transport)
                            self._wrapped_transport = NULL
                            raise PJSIPError("Could not create ZRTP media transport", status)
                    elif self._encryption == 'opportunistic':
                        # Build a stacked transport chain so the offer
                        # advertises BOTH SDES (a=crypto) and ZRTP
                        # (a=zrtp-hash), and the SDK can fall back between
                        # them based on the remote's answer.
                        #
                        # Stack (outside-in):
                        #     SRTP(OPTIONAL)  <- self._obj after this block
                        #         ZRTP        <- self._zrtp_transport
                        #             UDP/ICE <- wrapped_transport
                        #
                        # Behaviour:
                        #  - SRTP in OPTIONAL mode keeps the offer at
                        #    RTP/AVP and only activates when the answer
                        #    contains a=crypto.
                        #  - If SDES does NOT activate, packets pass
                        #    through the SRTP layer to ZRTP underneath.
                        #    ZRTP is initialised with autoEnable=0; the
                        #    Python layer calls set_zrtp_enabled(True)
                        #    once it has confirmed that SDES did not win.
                        #  - pjmedia_transport_encode_sdp recurses through
                        #    the chain, so the resulting offer carries
                        #    both a=crypto and a=zrtp-hash lines.
                        with nogil:
                            status = pjmedia_transport_zrtp_create(media_endpoint, pjsip_endpt_get_timer_heap(sip_endpoint), wrapped_transport, &self._zrtp_transport, 1)
                        if status != 0:
                            with nogil:
                                pjmedia_transport_close(wrapped_transport)
                            self._wrapped_transport = NULL
                            raise PJSIPError("Could not create ZRTP media transport for opportunistic encryption", status)
                        zid_file = ua.zrtp_cache.encode(sys.getfilesystemencoding())
                        c_zid_file = zid_file
                        with nogil:
                            # autoEnable=0: ZRTP only starts when the Python
                            # layer explicitly calls set_zrtp_enabled(True)
                            # after observing that SDES did not negotiate.
                            status = pjmedia_transport_zrtp_initialize(self._zrtp_transport, c_zid_file, 0, &_zrtp_cb)
                        if status != 0:
                            with nogil:
                                pjmedia_transport_close(self._zrtp_transport)
                            self._zrtp_transport = NULL
                            self._wrapped_transport = NULL
                            raise PJSIPError("Could not initialize ZRTP for opportunistic encryption", status)
                        # Now wrap the ZRTP transport with SRTP in OPTIONAL
                        # mode so a=crypto is added to the offer too. The
                        # default `use` value populated by
                        # pjmedia_srtp_setting_default is OPTIONAL; we set
                        # it explicitly below for clarity. close_member_tp
                        # defaults to PJ_TRUE, which is what we want.
                        with nogil:
                            pjmedia_srtp_setting_default(&srtp_setting)
                        srtp_setting.use = PJMEDIA_SRTP_OPTIONAL
                        # Offer only AES_CM_128_HMAC_SHA1_80 (matches the
                        # plain 'sdes' arm above and works around peers
                        # that mishandle multi-crypto offers).
                        srtp_setting.crypto_count = 1
                        _str_to_pj_str(b"AES_CM_128_HMAC_SHA1_80", &srtp_setting.crypto[0].name)
                        srtp_setting.crypto[0].key.ptr = NULL
                        srtp_setting.crypto[0].key.slen = 0
                        srtp_setting.crypto[0].flags = 0
                        # ZRTP callbacks (secure_on, show_sas, ...) receive
                        # the ZRTP transport pointer; _extract_rtp_transport
                        # reads tp->user_data to find the Python wrapper, so
                        # we set it on the ZRTP transport too. The outermost
                        # transport's user_data is set below (line ~681).
                        self._zrtp_transport.user_data = <void *> self.weakref
                        with nogil:
                            status = pjmedia_transport_srtp_create(media_endpoint, self._zrtp_transport, &srtp_setting, transport_address)
                        if status != 0:
                            with nogil:
                                # Closing ZRTP also closes its slave UDP
                                # because close_slave=1 was passed to
                                # pjmedia_transport_zrtp_create above.
                                pjmedia_transport_close(self._zrtp_transport)
                            self._zrtp_transport = NULL
                            self._wrapped_transport = NULL
                            raise PJSIPError("Could not create SRTP media transport for opportunistic encryption", status)
                        # Wrap the outermost transport (the SRTP_OPTIONAL
                        # layer) with our Sylk AEAD adapter so the same
                        # set_aead_keys path that works for plain 'sdes'
                        # also works in 'opportunistic' mode. Final stack
                        # then becomes:
                        #     Sylk AEAD adapter (passthrough until keys)
                        #         SRTP(OPTIONAL)
                        #             ZRTP
                        #                 UDP / ICE
                        # The adapter starts in passthrough so if SDES wins
                        # the answer, nothing changes on the wire until
                        # Python flips it to active via set_aead_keys
                        # (post Sylk-ZRTP-over-SIP-MESSAGE handshake).
                        srtp_transport = self._obj
                        self._obj = NULL
                        with nogil:
                            status = sylk_aead_transport_create(media_endpoint, NULL, srtp_transport, 1, transport_address)
                        if status != 0:
                            with nogil:
                                pjmedia_transport_close(srtp_transport)
                            self._zrtp_transport = NULL
                            self._wrapped_transport = NULL
                            raise PJSIPError("Could not create Sylk AEAD adapter (opportunistic)", status)
                    else:
                        raise RuntimeError('invalid SRTP key negotiation specified: %s' % self._encryption)
                    self._obj.user_data = <void *> self.weakref
                if not self.use_ice or self.ice_stun_address is None:
                    self.state = "INIT"
                    _add_event("RTPTransportDidInitialize", dict(obj=self))
                else:
                    self.state = "WAIT_STUN"
                if self.use_ice:
                    _add_event("RTPTransportICENegotiationStateDidChange", dict(obj=self, prev_state="NULL", state="GATHERING"))
                    ice_st = pjmedia_ice_get_strans(transport_address[0])
                    if ice_st != NULL:
                        ice_state = pj_ice_strans_get_state(ice_st)
                        if ice_state == PJ_ICE_STRANS_STATE_READY:
                            _add_event("RTPTransportICENegotiationStateDidChange", dict(obj=self, prev_state="GATHERING", state="GATHERING_COMPLETE"))
            else:
                raise SIPCoreError('set_INIT can only be called in the "NULL", "LOCAL" and "ESTABLISHED" states, ' +
                                   'current state is "%s"' % self.state)
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def set_zrtp_sas_verified(self, verified):
        cdef int status
        cdef int c_verified
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_zrtp_info *zrtp_info
        cdef pjmedia_transport_info info
        cdef pjmedia_transport *zrtp_tp
        cdef PJSIPUA ua

        ua = self._check_ua()
        if ua is None:
            return False

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                return False
            self._get_info(&info)
            zrtp_info = <pjmedia_zrtp_info *> pjmedia_transport_info_get_spc_info(&info, PJMEDIA_TRANSPORT_TYPE_ZRTP)
            if zrtp_info == NULL or not bool(zrtp_info.active):
                return False
            c_verified = int(verified)
            # When the chain is stacked (opportunistic encryption), the ZRTP
            # transport sits underneath self._obj (the outer SRTP wrapper).
            # ZRTP control calls must target the ZRTP transport directly.
            zrtp_tp = self._zrtp_transport if self._zrtp_transport != NULL else self._obj
            with nogil:
                pjmedia_transport_zrtp_setSASVerified(zrtp_tp, c_verified)
            return True
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def set_zrtp_enabled(self, enabled, object master_stream):
        cdef int status
        cdef int c_enabled
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_zrtp_info *zrtp_info
        cdef pjmedia_transport_info info
        cdef pjmedia_transport *zrtp_tp
        cdef pjmedia_transport *master_zrtp_tp
        cdef PJSIPUA ua
        cdef bytes multistream_params
        cdef char *c_multistream_params
        cdef int length
        cdef RTPTransport master_transport

        ua = self._check_ua()
        if ua is None:
            return

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                return
            self._get_info(&info)
            zrtp_info = <pjmedia_zrtp_info *> pjmedia_transport_info_get_spc_info(&info, PJMEDIA_TRANSPORT_TYPE_ZRTP)
            if zrtp_info == NULL:
                return
            # When the chain is stacked (opportunistic encryption), the ZRTP
            # transport sits underneath self._obj. Route control calls there.
            zrtp_tp = self._zrtp_transport if self._zrtp_transport != NULL else self._obj
            if master_stream is not None:
                master_transport = master_stream._rtp_transport
                assert master_transport is not None
                # extract the multistream parameters
                multistream_params = master_transport.zrtp_multistream_parameters
                if multistream_params:
                    # set multistream mode in ourselves
                    c_multistream_params = multistream_params
                    length = len(multistream_params)
                    master_zrtp_tp = master_transport._zrtp_transport if master_transport._zrtp_transport != NULL else master_transport._obj
                    with nogil:
                        pjmedia_transport_zrtp_setMultiStreamParameters(zrtp_tp, c_multistream_params, length, master_zrtp_tp)
            c_enabled = int(enabled)
            with nogil:
                pjmedia_transport_zrtp_setEnableZrtp(zrtp_tp, c_enabled)
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    property zrtp_multistream_parameters:

        def __get__(self):
            cdef int status
            cdef char* c_name
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_zrtp_info *zrtp_info
            cdef pjmedia_transport_info info
            cdef pjmedia_transport *zrtp_tp
            cdef PJSIPUA ua
            cdef char *multistr_params
            cdef int length

            ua = self._check_ua()
            if ua is None:
                return None

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                    return None
                self._get_info(&info)
                zrtp_info = <pjmedia_zrtp_info *> pjmedia_transport_info_get_spc_info(&info, PJMEDIA_TRANSPORT_TYPE_ZRTP)
                if zrtp_info == NULL or not bool(zrtp_info.active):
                    return None
                # Route through the inner ZRTP transport when stacked.
                zrtp_tp = self._zrtp_transport if self._zrtp_transport != NULL else self._obj
                with nogil:
                    multistr_params = pjmedia_transport_zrtp_getMultiStreamParameters(zrtp_tp, &length)
                if length > 0:
                    ret = _pj_buf_len_to_str(multistr_params, length)
                    free(multistr_params)
                    return ret
                else:
                    return None
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    property zrtp_cipher:

        def __get__(self):
            cdef int status
            cdef char* c_name
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_zrtp_info *zrtp_info
            cdef pjmedia_transport_info info
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return None

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                    return None
                self._get_info(&info)
                zrtp_info = <pjmedia_zrtp_info *> pjmedia_transport_info_get_spc_info(&info, PJMEDIA_TRANSPORT_TYPE_ZRTP)
                if zrtp_info == NULL or not bool(zrtp_info.active):
                    return None
                return _buf_to_str(zrtp_info.cipher)
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    property zrtp_peer_name:

        def __get__(self):
            cdef int status
            cdef char* c_name
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_zrtp_info *zrtp_info
            cdef pjmedia_transport_info info
            cdef pjmedia_transport *zrtp_tp
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return ''

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                    return ''
                self._get_info(&info)
                zrtp_info = <pjmedia_zrtp_info *> pjmedia_transport_info_get_spc_info(&info, PJMEDIA_TRANSPORT_TYPE_ZRTP)
                if zrtp_info == NULL or not bool(zrtp_info.active):
                    return ''
                # Route through the inner ZRTP transport when stacked.
                zrtp_tp = self._zrtp_transport if self._zrtp_transport != NULL else self._obj
                with nogil:
                    c_name = pjmedia_transport_zrtp_getPeerName(zrtp_tp)
                if c_name == NULL:
                    return ''
                else:
                    name = PyUnicode_FromString(c_name) or u''
                    free(c_name)
                    return name
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

        def __set__(self, object name):
            cdef int status
            cdef char* c_name
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_zrtp_info *zrtp_info
            cdef pjmedia_transport_info info
            cdef pjmedia_transport *zrtp_tp
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                    return
                self._get_info(&info)
                zrtp_info = <pjmedia_zrtp_info *> pjmedia_transport_info_get_spc_info(&info, PJMEDIA_TRANSPORT_TYPE_ZRTP)
                if zrtp_info == NULL or not bool(zrtp_info.active):
                    return
                c_name = name
                # Route through the inner ZRTP transport when stacked.
                zrtp_tp = self._zrtp_transport if self._zrtp_transport != NULL else self._obj
                with nogil:
                    pjmedia_transport_zrtp_putPeerName(zrtp_tp, c_name)
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    property zrtp_peer_id:

        def __get__(self):
            cdef int status
            cdef unsigned char name[12]    # IDENTIFIER_LEN, 96bits
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_zrtp_info *zrtp_info
            cdef pjmedia_transport_info info
            cdef pjmedia_transport *zrtp_tp
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return None

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                if self.state in ["NULL", "WAIT_STUN", "INVALID"]:
                    return None
                self._get_info(&info)
                zrtp_info = <pjmedia_zrtp_info *> pjmedia_transport_info_get_spc_info(&info, PJMEDIA_TRANSPORT_TYPE_ZRTP)
                if zrtp_info == NULL or not bool(zrtp_info.active):
                    return None
                # Route through the inner ZRTP transport when stacked.
                zrtp_tp = self._zrtp_transport if self._zrtp_transport != NULL else self._obj
                with nogil:
                    status = pjmedia_transport_zrtp_getPeerZid(zrtp_tp, name)
                if status <= 0:
                    return None
                else:
                    return _pj_buf_len_to_str(<char*>name, 12)
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    def update_local_sdp(self, SDPSession local_sdp, BaseSDPSession remote_sdp=None, int sdp_index=0):
        cdef int status
        cdef pj_pool_t *pool
        cdef pjmedia_sdp_session *pj_local_sdp
        cdef pjmedia_sdp_session *pj_remote_sdp
        cdef pjmedia_transport *transport
        cdef SDPMediaStream local_media

        pool = self._pool
        transport = self._obj
        pj_local_sdp = local_sdp.get_sdp_session()
        if remote_sdp is not None:
            pj_remote_sdp = remote_sdp.get_sdp_session()
        else:
            pj_remote_sdp = NULL
        if sdp_index < 0:
            raise ValueError("sdp_index argument cannot be negative")
        if sdp_index >= <int>pj_local_sdp.media_count:
            raise ValueError("sdp_index argument out of range")
        # Remove ICE and SRTP/ZRTP related attributes from SDP, they will be added by pjmedia_transport_encode_sdp
        local_media = local_sdp.media[sdp_index]
        local_media.attributes = [<object> attr for attr in local_media.attributes if attr.name not in ('crypto', 'zrtp-hash', 'ice-ufrag', 'ice-pwd', 'ice-mismatch', 'candidate', 'remote-candidates')]
        pj_local_sdp = local_sdp.get_sdp_session()

        with nogil:
            status = pjmedia_transport_encode_sdp(transport, pool, pj_local_sdp, pj_remote_sdp, sdp_index)
        if status != 0:
            raise PJSIPError("Could not update SDP for media transport", status)
        local_sdp._update()
        return 0


cdef class MediaCheckTimer(Timer):
    def __init__(self, media_check_interval):
        self.media_check_interval = media_check_interval


cdef class SDPInfo:
    def __init__(self, BaseSDPMediaStream local_media=None, BaseSDPSession local_sdp=None, BaseSDPSession remote_sdp=None, int index=0):
        self.local_media = local_media
        self.local_sdp = local_sdp
        self.remote_sdp = remote_sdp
        self.index = index

    property local_media:

        def __get__(self):
            return self._local_media

        def __set__(self, local_media):
            if local_media is not None:
                local_media = SDPMediaStream.new(local_media)
            self._local_media = local_media

    property local_sdp:

        def __get__(self):
            return self._local_sdp

        def __set__(self, local_sdp):
            if local_sdp is not None:
                local_sdp = SDPSession.new(local_sdp)
            self._local_sdp = local_sdp

    property remote_sdp:

        def __get__(self):
            return self._remote_sdp

        def __set__(self, remote_sdp):
            if remote_sdp is not None:
                remote_sdp = SDPSession.new(remote_sdp)
            self._remote_sdp = remote_sdp


cdef class AudioTransport:
    def __cinit__(self, *args, **kwargs):
        cdef int status
        cdef pj_pool_t *pool
        cdef bytes pool_name
        cdef char* c_pool_name
        cdef PJSIPUA ua

        ua = _get_ua()
        pool_name = b"AudioTransport_%d" % id(self)

        self.weakref = weakref.ref(self)
        Py_INCREF(self.weakref)

        status = pj_mutex_create_recursive(ua._pjsip_endpoint._pool, "audio_transport_lock", &self._lock)
        if status != 0:
            raise PJSIPError("failed to create lock", status)

        pool = ua.create_memory_pool(pool_name, 4096, 4096)
        self._pool = pool
        self._slot = -1
        self._timer = None
        self._volume = 100

    def __init__(self, AudioMixer mixer, RTPTransport transport,
                 BaseSDPSession remote_sdp=None, int sdp_index=0, enable_silence_detection=False, list codecs=None):
        cdef int status
        cdef pj_pool_t *pool
        cdef pjmedia_endpt *media_endpoint
        cdef pjmedia_sdp_media *local_media_c
        cdef pjmedia_sdp_session *local_sdp_c
        cdef pj_sockaddr *addr
        cdef pjmedia_transport_info info
        cdef list global_codecs
        cdef SDPMediaStream local_media
        cdef SDPSession local_sdp
        cdef PJSIPUA ua

        ua = _get_ua()
        media_endpoint = ua._pjmedia_endpoint._obj
        pool = self._pool

        if self.transport is not None:
            raise SIPCoreError("AudioTransport.__init__() was already called")
        if mixer is None:
            raise ValueError("mixer argument may not be None")
        if transport is None:
            raise ValueError("transport argument cannot be None")
        if sdp_index < 0:
            raise ValueError("sdp_index argument cannot be negative")
        if transport.state != "INIT":
            raise SIPCoreError('RTPTransport object provided is not in the "INIT" state, but in the "%s" state' %
                               transport.state)
        self._vad = int(bool(enable_silence_detection))
        self.mixer = mixer
        self.transport = transport
        transport._get_info(&info)
        global_codecs = ua._pjmedia_endpoint._get_current_codecs()
        if codecs is None:
            codecs = global_codecs
        try:
            ua._pjmedia_endpoint._set_codecs(codecs)
            addr = &info.sock_info.rtp_addr_name
            with nogil:
                status = pjmedia_endpt_create_base_sdp(media_endpoint, pool, NULL, addr, &local_sdp_c)
            if status != 0:
                raise PJSIPError("Could not generate base SDP", status)
            with nogil:
                status = pjmedia_endpt_create_audio_sdp(media_endpoint, pool, &info.sock_info, 0, &local_media_c)
            if status != 0:
                raise PJSIPError("Could not generate SDP audio stream", status)
            # Create a 'fake' SDP, which only contains the audio stream, then the m line is extracted because the full
            # SDP is built by the Session
            local_sdp_c.media_count = 1
            local_sdp_c.media[0] = local_media_c
        finally:
            ua._pjmedia_endpoint._set_codecs(global_codecs)
        local_sdp = SDPSession_create(local_sdp_c)
        local_media = local_sdp.media[0]
        if remote_sdp is None:
            self._is_offer = 1
            self.transport.set_LOCAL(local_sdp, 0)
        else:
            self._is_offer = 0
            if sdp_index != 0:
                local_sdp.media = [None] * (sdp_index+1)
                local_sdp.media[sdp_index] = local_media
            self.transport.set_REMOTE(local_sdp, remote_sdp, sdp_index)
        self._sdp_info = SDPInfo(local_media, local_sdp, remote_sdp, sdp_index)

    def __dealloc__(self):
        cdef PJSIPUA ua
        cdef Timer timer
        try:
            ua = _get_ua()
        except:
            return
        if self._obj != NULL:
            self.stop()
        ua.release_memory_pool(self._pool)
        self._pool = NULL
        if self._lock != NULL:
            pj_mutex_destroy(self._lock)
        timer = Timer()
        try:
            timer.schedule(60, deallocate_weakref, self.weakref)
        except SIPCoreError:
            pass

    cdef PJSIPUA _check_ua(self):
        cdef PJSIPUA ua
        try:
            ua = _get_ua()
            return ua
        except:
            self._obj = NULL
            self._pool = NULL
            return None

    property is_active:

        def __get__(self):
            self._check_ua()
            return bool(self._obj != NULL)

    property is_started:

        def __get__(self):
            return bool(self._is_started)

    property codec:

        def __get__(self):
            self._check_ua()
            if self._obj == NULL:
                return None
            else:
                return _pj_str_to_bytes(self._stream_info.fmt.encoding_name)

    property sample_rate:

        def __get__(self):
            self._check_ua()
            if self._obj == NULL:
                return None
            else:
                return self._stream_info.fmt.clock_rate

    property enable_silence_detection:

        def __get__(self):
            return bool(self._vad)

    property statistics:

        def __get__(self):
            cdef int status
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_rtcp_stat stat
            cdef pjmedia_stream *stream
            cdef dict statistics = dict()
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return None

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                stream = self._obj

                if stream == NULL:
                    return None

                with nogil:
                    status = pjmedia_stream_get_stat(stream, &stat)
                if status != 0:
                    raise PJSIPError("Could not get RTP statistics", status)
                statistics["rtt"] = _pj_math_stat_to_dict(&stat.rtt)
                statistics["rx"] = _pjmedia_rtcp_stream_stat_to_dict(&stat.rx)
                statistics["tx"] = _pjmedia_rtcp_stream_stat_to_dict(&stat.tx)
                return statistics
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    property volume:

        def __get__(self):
            return self._volume

        def __set__(self, value):
            cdef int slot
            cdef int status
            cdef int volume
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_conf *conf_bridge
            cdef PJSIPUA ua

            ua = self._check_ua()

            if ua is not None:
                with nogil:
                    status = pj_mutex_lock(lock)
                if status != 0:
                    raise PJSIPError("failed to acquire lock", status)
            try:
                conf_bridge = self.mixer._obj
                slot = self._slot

                if value < 0:
                    raise ValueError("volume attribute cannot be negative")
                if ua is not None and self._obj != NULL:
                    volume = int(value * 1.28 - 128)
                    with nogil:
                        status = pjmedia_conf_adjust_rx_level(conf_bridge, slot, volume)
                    if status != 0:
                        raise PJSIPError("Could not set volume of audio transport", status)
                self._volume = value
            finally:
                if ua is not None:
                    with nogil:
                        pj_mutex_unlock(lock)

    property slot:

        def __get__(self):
            self._check_ua()
            if self._slot == -1:
                return None
            else:
                return self._slot

    def get_local_media(self, BaseSDPSession remote_sdp=None, int index=0, direction="sendrecv"):
        global valid_sdp_directions
        cdef int status
        cdef pj_mutex_t *lock = self._lock
        cdef object direction_attr
        cdef SDPAttribute attr
        cdef SDPSession local_sdp
        cdef SDPMediaStream local_media
        cdef pjmedia_sdp_media *c_local_media

        _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            is_offer = remote_sdp is None
            if is_offer and direction not in valid_sdp_directions:
                raise SIPCoreError("Unknown direction: %s" % direction)
            self._sdp_info.index = index
            local_sdp = self._sdp_info.local_sdp
            local_media = self._sdp_info.local_media
            local_sdp.media = [None] * (index+1)
            local_sdp.media[index] = local_media
            self.transport.update_local_sdp(local_sdp, remote_sdp, index)
            # updating the local SDP might have modified the connection line
            if local_sdp.connection is not None and local_media.connection is None:
                local_media.connection = SDPConnection.new(local_sdp.connection)
            local_media.attributes = [<object> attr for attr in local_media.attributes if attr.name not in valid_sdp_directions]
            if is_offer:
                direction_attr = direction
            else:
                if self.direction is None or "recv" in self.direction.decode():
                    direction_attr = b"sendrecv"
                else:
                    direction_attr = b"sendonly"
            local_media.attributes.append(SDPAttribute(direction_attr, b""))
            for attribute in local_media.attributes:
                if attribute.name == b'rtcp':
                    attribute.value = (attribute.value.decode().split(' ', 1)[0]).encode()
            self._sdp_info.local_media = local_media
            return local_media
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def start(self, BaseSDPSession local_sdp, BaseSDPSession remote_sdp, int sdp_index, int timeout=30):
        cdef int status
        cdef object desired_state
        cdef pj_mutex_t *lock = self._lock
        cdef pj_pool_t *pool
        cdef pjmedia_endpt *media_endpoint
        cdef pjmedia_port *media_port
        cdef pjmedia_sdp_media *local_media
        cdef pjmedia_sdp_session *pj_local_sdp
        cdef pjmedia_sdp_session *pj_remote_sdp
        cdef pjmedia_stream **stream_address
        cdef pjmedia_stream_info *stream_info_address
        cdef pjmedia_transport *transport
        cdef PJSIPUA ua

        ua = _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            pool = self._pool
            media_endpoint = ua._pjmedia_endpoint._obj
            stream_address = &self._obj
            stream_info_address = &self._stream_info
            transport = self.transport._obj

            if self._is_started:
                raise SIPCoreError("This AudioTransport was already started once")
            desired_state = ("LOCAL" if self._is_offer else "REMOTE")
            if self.transport.state != desired_state:
                raise SIPCoreError('RTPTransport object provided is not in the "%s" state, but in the "%s" state' %
                                   (desired_state, self.transport.state))
            if None in [local_sdp, remote_sdp]:
                raise ValueError("SDP arguments cannot be None")
            pj_local_sdp = local_sdp.get_sdp_session()
            pj_remote_sdp = remote_sdp.get_sdp_session()
            if sdp_index < 0:
                raise ValueError("sdp_index argument cannot be negative")
            if local_sdp.media[sdp_index].port == 0 or remote_sdp.media[sdp_index].port == 0:
                raise SIPCoreError("Cannot start a rejected audio stream")
            if timeout < 0:
                raise ValueError("timeout value cannot be negative")
            self.transport.set_ESTABLISHED(local_sdp, remote_sdp, sdp_index)
            with nogil:
                status = pjmedia_stream_info_from_sdp(stream_info_address, pool, media_endpoint,
                                                      pj_local_sdp, pj_remote_sdp, sdp_index)
            if status != 0:
                raise PJSIPError("Could not parse SDP for audio session", status)
            if self._stream_info.param == NULL:
                raise SIPCoreError("Could not parse SDP for audio session")
            self._stream_info.param.setting.vad = self._vad
            self._stream_info.use_ka = 1
            # pjsip 2.12 added pjmedia_stream_info::ka_cfg. pjmedia_stream_info_from_sdp()
            # zeroes the whole struct, which leaves ka_interval=0 -- and stream.c then
            # treats that as "fire a 12-byte RTP keep-alive before every audio frame",
            # flooding the wire and breaking interop. Restore the documented pjmedia
            # defaults (PJMEDIA_STREAM_KA_INTERVAL=5s, START_KA_CNT=2, START_INTERVAL=1000ms).
            pjmedia_stream_ka_config_default(&self._stream_info.ka_cfg)
            with nogil:
                status = pjmedia_stream_create(media_endpoint, pool, stream_info_address,
                                               transport, NULL, stream_address)
            if status != 0:
                raise PJSIPError("Could not initialize RTP for audio session", status)
            with nogil:
                status = pjmedia_stream_set_dtmf_callback(stream_address[0], _AudioTransport_cb_dtmf, <void *> self.weakref)
            if status != 0:
                with nogil:
                    pjmedia_stream_destroy(stream_address[0])
                self._obj = NULL
                raise PJSIPError("Could not set DTMF callback for audio session", status)
            with nogil:
                status = pjmedia_stream_start(stream_address[0])
            if status != 0:
                with nogil:
                    pjmedia_stream_destroy(stream_address[0])
                self._obj = NULL
                raise PJSIPError("Could not start RTP for audio session", status)
            with nogil:
                status = pjmedia_stream_get_port(stream_address[0], &media_port)
            if status != 0:
                with nogil:
                    pjmedia_stream_destroy(stream_address[0])
                self._obj = NULL
                raise PJSIPError("Could not get audio port for audio session", status)
            try:
                self._slot = self.mixer._add_port(ua, pool, media_port)
                if self._volume != 100:
                    self.volume = self._volume
            except:
                with nogil:
                    pjmedia_stream_destroy(stream_address[0])
                self._obj = NULL
                raise
            self.update_direction(local_sdp.media[sdp_index].direction)
            self._sdp_info.local_media = local_sdp.media[sdp_index]
            self._sdp_info.local_sdp = local_sdp
            self._sdp_info.remote_sdp = remote_sdp
            self._sdp_info.index = sdp_index
            self._is_started = 1
            if timeout > 0:
                self._timer = MediaCheckTimer(timeout)
                self._timer.schedule(timeout, <timer_callback>self._cb_check_rtp, self)
            self.mixer.reset_ec()
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def stop(self):
        cdef int status
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_stream *stream
        cdef PJSIPUA ua

        ua = self._check_ua()

        if ua is not None:
            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
        try:
            stream = self._obj

            if self._timer is not None:
                self._timer.cancel()
                self._timer = None
            if self._obj == NULL:
                return
            self._obj = NULL
            self.mixer._remove_port(ua, self._slot)
            with nogil:
                pjmedia_stream_destroy(stream)
            self.transport.set_INIT()
        finally:
            if ua is not None:
                with nogil:
                    pj_mutex_unlock(lock)

    def update_direction(self, direction):
        cdef int status
        cdef pj_mutex_t *lock = self._lock

        _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            if self._obj == NULL:
                raise SIPCoreError("Stream is not active")
            if direction not in valid_sdp_directions:
                raise SIPCoreError("Unknown direction: %s" % direction)
            if direction != self.direction:
                self.mixer.reset_ec()
            self.direction = direction
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def update_sdp(self, local_sdp, remote_sdp, index):
        cdef int status
        cdef pj_mutex_t *lock = self._lock

        _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            if self._obj == NULL:
                raise SIPCoreError("Stream is not active")
            self._sdp_info.local_media = local_sdp.media[index]
            self._sdp_info.local_sdp = local_sdp
            self._sdp_info.remote_sdp = remote_sdp
            self._sdp_info.index = index
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def send_dtmf(self, digit):
        cdef int status
        cdef pj_mutex_t *lock = self._lock
        cdef pj_str_t digit_pj
        cdef pjmedia_stream *stream
        cdef PJSIPUA ua

        ua = _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            stream = self._obj

            if self._obj == NULL:
                raise SIPCoreError("Stream is not active")
            if len(digit) != 1 or digit not in "0123456789*#ABCD":
                raise SIPCoreError("Not a valid DTMF digit: %s" % digit)
            _str_to_pj_str(digit.encode(), &digit_pj)
            if not self._stream_info.tx_event_pt < 0:
                # If the remote doesn't support telephone-event just don't send DTMF
                with nogil:
                    status = pjmedia_stream_dial_dtmf(stream, &digit_pj)
                if status != 0:
                    raise PJSIPError("Could not send DTMF digit on audio stream", status)
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    cdef int _cb_check_rtp(self, MediaCheckTimer timer) except -1:
        cdef int status
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_rtcp_stat stat
        cdef pjmedia_stream *stream

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            stream = self._obj
            if stream == NULL:
                return 0

            if self._timer is None:
                return 0
            self._timer = None
            with nogil:
                status = pjmedia_stream_get_stat(stream, &stat)
            if status == 0:
                if self._packets_received == stat.rx.pkt and self.direction == "sendrecv":
                    _add_event("RTPAudioTransportDidTimeout", dict(obj=self))
                self._packets_received = stat.rx.pkt
                if timer.media_check_interval > 0:
                    self._timer = MediaCheckTimer(timer.media_check_interval)
                    self._timer.schedule(timer.media_check_interval, <timer_callback>self._cb_check_rtp, self)
        finally:
            with nogil:
                pj_mutex_unlock(lock)


cdef class VideoTransport:

    def __cinit__(self, *args, **kwargs):
        cdef int status
        cdef pj_pool_t *pool
        cdef bytes pool_name
        cdef PJSIPUA ua

        ua = _get_ua()
        endpoint = ua._pjsip_endpoint._obj
        pool_name = b"VideoTransport_%d" % id(self)

        self.weakref = weakref.ref(self)
        Py_INCREF(self.weakref)

        pool = ua.create_memory_pool(pool_name, 4096, 4096)
        self._pool = pool

        status = pj_mutex_create_recursive(pool, "video_transport_lock", &self._lock)
        if status != 0:
            raise PJSIPError("failed to create lock", status)

        self._timer = None

    def __init__(self, RTPTransport transport, BaseSDPSession remote_sdp=None, int sdp_index=0, list codecs=None):
        cdef int status
        cdef pj_pool_t *pool
        cdef pjmedia_endpt *media_endpoint
        cdef pjmedia_sdp_media *local_media_c
        cdef pjmedia_sdp_session *local_sdp_c
        cdef pjmedia_transport_info info
        cdef pj_sockaddr *addr
        cdef list global_codecs
        cdef SDPMediaStream local_media
        cdef SDPSession local_sdp
        cdef PJSIPUA ua

        ua = _get_ua()
        media_endpoint = ua._pjmedia_endpoint._obj
        pool = self._pool

        if self.transport is not None:
            raise SIPCoreError("VideoTransport.__init__() was already called")
        if transport is None:
            raise ValueError("transport argument cannot be None")
        if sdp_index < 0:
            raise ValueError("sdp_index argument cannot be negative")
        if transport.state != "INIT":
            raise SIPCoreError('RTPTransport object provided is not in the "INIT" state, but in the "%s" state' % transport.state)
        self.transport = transport
        transport._get_info(&info)
        global_codecs = ua._pjmedia_endpoint._get_current_video_codecs()
        if codecs is None:
            codecs = global_codecs
        try:
            ua._pjmedia_endpoint._set_video_codecs(codecs)
            addr = &(info.sock_info.rtp_addr_name)
            with nogil:
                status = pjmedia_endpt_create_base_sdp(media_endpoint, pool, NULL, addr, &local_sdp_c)
            if status != 0:
                raise PJSIPError("Could not generate base SDP", status)
            with nogil:
                status = pjmedia_endpt_create_video_sdp(media_endpoint, pool, &info.sock_info, 0, &local_media_c)
            if status != 0:
                raise PJSIPError("Could not generate SDP video stream", status)
            # Create a 'fake' SDP, which only contains the video stream, then the m line is extracted because the full
            # SDP is built by the Session
            local_sdp_c.media_count = 1
            local_sdp_c.media[0] = local_media_c
        finally:
            ua._pjmedia_endpoint._set_video_codecs(global_codecs)
        local_sdp = SDPSession_create(local_sdp_c)
        local_media = local_sdp.media[0]
        if remote_sdp is None:
            self._is_offer = 1
            self.transport.set_LOCAL(local_sdp, 0)
        else:
            self._is_offer = 0
            if sdp_index != 0:
                local_sdp.media = [None] * (sdp_index+1)
                local_sdp.media[sdp_index] = local_media
            self.transport.set_REMOTE(local_sdp, remote_sdp, sdp_index)
        self._sdp_info = SDPInfo(local_media, local_sdp, remote_sdp, sdp_index)

        self.local_video = None
        self.remote_video = None

    def __dealloc__(self):
        cdef PJSIPUA ua
        cdef Timer timer
        try:
            ua = _get_ua()
        except SIPCoreError:
            return
        if self._obj != NULL:
            self.stop()
        if self._lock != NULL:
            pj_mutex_destroy(self._lock)
        ua.release_memory_pool(self._pool)
        self._pool = NULL
        timer = Timer()
        try:
            timer.schedule(60, deallocate_weakref, self.weakref)
        except SIPCoreError:
            pass

    cdef PJSIPUA _check_ua(self):
        cdef PJSIPUA ua
        try:
            ua = _get_ua()
            return ua
        except:
            self._obj = NULL
            self._pool = NULL
            return None

    property is_active:

        def __get__(self):
            self._check_ua()
            return bool(self._obj != NULL)

    property is_started:

        def __get__(self):
            return bool(self._is_started)

    property codec:

        def __get__(self):
            self._check_ua()
            if self._obj == NULL:
                return None
            else:
                return _pj_str_to_bytes(self._stream_info.codec_info.encoding_name)

    property sample_rate:

        def __get__(self):
            self._check_ua()
            if self._obj == NULL:
                return None
            else:
                return self._stream_info.codec_info.clock_rate

    property statistics:

        def __get__(self):
            cdef int status
            cdef pj_mutex_t *lock = self._lock
            cdef pjmedia_rtcp_stat stat
            cdef pjmedia_vid_stream *stream
            cdef dict statistics = dict()
            cdef PJSIPUA ua

            ua = self._check_ua()
            if ua is None:
                return None

            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
            try:
                stream = self._obj

                if stream == NULL:
                    return None

                with nogil:
                    status = pjmedia_vid_stream_get_stat(stream, &stat)
                if status != 0:
                    raise PJSIPError("Could not get RTP statistics", status)
                statistics["rtt"] = _pj_math_stat_to_dict(&stat.rtt)
                statistics["rx"] = _pjmedia_rtcp_stream_stat_to_dict(&stat.rx)
                statistics["tx"] = _pjmedia_rtcp_stream_stat_to_dict(&stat.tx)
                return statistics
            finally:
                with nogil:
                    pj_mutex_unlock(lock)

    def get_local_media(self, BaseSDPSession remote_sdp=None, int index=0, direction="sendrecv"):
        global valid_sdp_directions
        cdef int status
        cdef pj_mutex_t *lock = self._lock
        cdef object direction_attr
        cdef SDPAttribute attr
        cdef SDPSession local_sdp
        cdef SDPMediaStream local_media
        cdef pjmedia_sdp_media *c_local_media

        _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            is_offer = remote_sdp is None
            if is_offer and direction not in valid_sdp_directions:
                raise SIPCoreError("Unknown direction: %s" % direction)
            self._sdp_info.index = index
            local_sdp = self._sdp_info.local_sdp
            local_media = self._sdp_info.local_media
            local_sdp.media = [None] * (index+1)
            local_sdp.media[index] = local_media
            self.transport.update_local_sdp(local_sdp, remote_sdp, index)
            # updating the local SDP might have modified the connection line
            if local_sdp.connection is not None and local_media.connection is None:
                local_media.connection = SDPConnection.new(local_sdp.connection)
            local_media.attributes = [<object> attr for attr in local_media.attributes if attr.name not in valid_sdp_directions]
            if is_offer:
                direction_attr = direction
            else:
                if self.direction is None or "recv" in self.direction.decode():
                    direction_attr = b"sendrecv"
                else:
                    direction_attr = b"sendonly"
            local_media.attributes.append(SDPAttribute(direction_attr, b""))
            for attribute in local_media.attributes:
                if attribute.name == 'rtcp':
                    attribute.value = (attribute.value.decode().split(' ', 1)[0]).encode()
            return local_media
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def start(self, BaseSDPSession local_sdp, BaseSDPSession remote_sdp, int sdp_index, int timeout=30):
        cdef int status
        cdef object desired_state
        cdef pj_mutex_t *lock = self._lock
        cdef pj_pool_t *pool
        cdef pjmedia_endpt *media_endpoint
        cdef pjmedia_sdp_media *local_media
        cdef pjmedia_sdp_session *pj_local_sdp
        cdef pjmedia_sdp_session *pj_remote_sdp
        cdef pjmedia_vid_stream *stream
        cdef pjmedia_vid_stream_info *stream_info
        cdef pjmedia_transport *transport
        cdef PJSIPUA ua

        ua = _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            pool = self._pool
            media_endpoint = ua._pjmedia_endpoint._obj
            stream_info = &self._stream_info
            transport = self.transport._obj

            if self._is_started:
                raise SIPCoreError("This VideoTransport was already started once")
            desired_state = ("LOCAL" if self._is_offer else "REMOTE")
            if self.transport.state != desired_state:
                raise SIPCoreError('RTPTransport object provided is not in the "%s" state, but in the "%s" state' % (desired_state, self.transport.state))
            if None in (local_sdp, remote_sdp):
                raise ValueError("SDP arguments cannot be None")
            pj_local_sdp = local_sdp.get_sdp_session()
            pj_remote_sdp = remote_sdp.get_sdp_session()
            if sdp_index < 0:
                raise ValueError("sdp_index argument cannot be negative")
            if local_sdp.media[sdp_index].port == 0 or remote_sdp.media[sdp_index].port == 0:
                raise SIPCoreError("Cannot start a rejected video stream")
            if timeout < 0:
                raise ValueError("timeout value cannot be negative")
            self.transport.set_ESTABLISHED(local_sdp, remote_sdp, sdp_index)
            with nogil:
                status = pjmedia_vid_stream_info_from_sdp(stream_info, pool, media_endpoint, pj_local_sdp, pj_remote_sdp, sdp_index)
            if status != 0:
                raise PJSIPError("Could not parse SDP for video session", status)
            if self._stream_info.codec_param == NULL:
                raise SIPCoreError("Could not parse SDP for video session")
            self._stream_info.use_ka = 1
            # See audio path above: pjsip 2.12 needs ka_cfg initialised explicitly,
            # otherwise stream.c fires a keep-alive before every frame.
            pjmedia_stream_ka_config_default(&self._stream_info.ka_cfg)
            with nogil:
                status = pjmedia_vid_stream_create(media_endpoint, pool, stream_info, transport, NULL, &stream)
            if status != 0:
                raise PJSIPError("Could not initialize RTP for video session", status)
            self._obj = stream
            with nogil:
                status = pjmedia_vid_stream_start(stream)
            if status != 0:
                with nogil:
                    pjmedia_vid_stream_destroy(stream)
                self._obj = NULL
                raise PJSIPError("Could not start RTP for video session", status)
            with nogil:
                pjmedia_vid_stream_send_rtcp_sdes(stream)
            try:
                local_video = LocalVideoStream_create(stream)
                remote_video = RemoteVideoStream_create(stream, self._remote_video_event_handler)
            except PJSIPError:
                with nogil:
                    pjmedia_vid_stream_destroy(stream)
                self._obj = NULL
                self.local_video = None
                self.remote_video = None
                raise
            self.local_video = local_video
            self.remote_video = remote_video
            self.update_direction(local_sdp.media[sdp_index].direction)
            self._sdp_info.local_media = local_sdp.media[sdp_index]
            self._sdp_info.local_sdp = local_sdp
            self._sdp_info.remote_sdp = remote_sdp
            self._sdp_info.index = sdp_index
            self._is_started = 1
            if timeout > 0:
                self._timer = MediaCheckTimer(timeout)
                self._timer.schedule(timeout, <timer_callback>self._cb_check_rtp, self)
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def stop(self):
        cdef int status
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_vid_stream *stream
        cdef PJSIPUA ua

        ua = self._check_ua()

        if ua is not None:
            with nogil:
                status = pj_mutex_lock(lock)
            if status != 0:
                raise PJSIPError("failed to acquire lock", status)
        try:
            stream = self._obj

            if self._timer is not None:
                self._timer.cancel()
                self._timer = None
            if self._obj == NULL:
                return
            self._obj = NULL
            if self.local_video is not None:
                self.local_video.close()
                self.local_video = None
            if self.remote_video is not None:
                self.remote_video.close()
                self.remote_video = None
            with nogil:
                pjmedia_vid_stream_send_rtcp_bye(stream)
                pjmedia_vid_stream_destroy(stream)
            self.transport.set_INIT()
        finally:
            if ua is not None:
                with nogil:
                    pj_mutex_unlock(lock)

    def update_direction(self, direction):
        global valid_sdp_directions
        cdef int status
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_vid_stream *stream

        _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            stream = self._obj

            if self._obj == NULL:
                raise SIPCoreError("Stream is not active")
            if direction not in valid_sdp_directions:
                raise SIPCoreError("Unknown direction: %s" % direction)
            self.direction = direction
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def update_sdp(self, local_sdp, remote_sdp, index):
        cdef int status
        cdef pj_mutex_t *lock = self._lock

        _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            if self._obj == NULL:
                raise SIPCoreError("Stream is not active")
            self._sdp_info.local_media = local_sdp.media[index]
            self._sdp_info.local_sdp = local_sdp
            self._sdp_info.remote_sdp = remote_sdp
            self._sdp_info.index = index
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def pause(self, direction="both"):
        cdef int status
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_vid_stream *stream
        cdef pjmedia_dir pj_dir

        _get_ua()

        if direction not in ("incoming", "outgoing", "both"):
            raise ValueError("direction can only be one of 'incoming', 'outgoing' or 'both'")

        if direction == "incoming":
            pj_dir = PJMEDIA_DIR_RENDER
        elif direction == "outgoing":
            pj_dir = PJMEDIA_DIR_CAPTURE
        else:
            pj_dir = PJMEDIA_DIR_CAPTURE_RENDER

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            stream = self._obj
            if self._obj == NULL:
                raise SIPCoreError("Stream is not active")
            with nogil:
                status = pjmedia_vid_stream_pause(stream, pj_dir)
            if status != 0:
                raise PJSIPError("failed to pause video stream", status)
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def resume(self, direction="both"):
        cdef int status
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_vid_stream *stream
        cdef pjmedia_dir pj_dir

        _get_ua()

        if direction not in ("incoming", "outgoing", "both"):
            raise ValueError("direction can only be one of 'incoming', 'outgoing' or 'both'")

        if direction == "incoming":
            pj_dir = PJMEDIA_DIR_RENDER
        elif direction == "outgoing":
            pj_dir = PJMEDIA_DIR_CAPTURE
        else:
            pj_dir = PJMEDIA_DIR_CAPTURE_RENDER

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            stream = self._obj
            if self._obj == NULL:
                raise SIPCoreError("Stream is not active")
            with nogil:
                status = pjmedia_vid_stream_resume(stream, pj_dir)
            if status != 0:
                raise PJSIPError("failed to resume video stream", status)
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def send_keyframe(self):
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_vid_stream *stream

        _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            stream = self._obj
            if stream != NULL:
                # Do not check for errors, it's OK if we can't send it
                pjmedia_vid_stream_send_keyframe(stream)
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def request_keyframe(self):
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_vid_stream *stream

        _get_ua()

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            stream = self._obj
            if stream != NULL:
                # Do not check for errors, it's OK if we can't send it
                pjmedia_vid_stream_send_rtcp_pli(stream)
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    cdef int _cb_check_rtp(self, MediaCheckTimer timer) except -1:
        cdef int status
        cdef pj_mutex_t *lock = self._lock
        cdef pjmedia_rtcp_stat stat
        cdef pjmedia_vid_stream *stream

        with nogil:
            status = pj_mutex_lock(lock)
        if status != 0:
            raise PJSIPError("failed to acquire lock", status)
        try:
            stream = self._obj
            if stream == NULL:
                return 0
            if self._timer is None:
                return 0
            self._timer = None
            with nogil:
                status = pjmedia_vid_stream_get_stat(stream, &stat)
            if status == 0:
                if self._packets_received == stat.rx.pkt and self.direction == "sendrecv":
                    _add_event("RTPVideoTransportDidTimeout", dict(obj=self))
                self._packets_received = stat.rx.pkt
                if timer.media_check_interval > 0:
                    self._timer = MediaCheckTimer(timer.media_check_interval)
                    self._timer.schedule(timer.media_check_interval, <timer_callback>self._cb_check_rtp, self)
        finally:
            with nogil:
                pj_mutex_unlock(lock)

    def _remote_video_event_handler(self, str name, object data):
        if name == "FORMAT_CHANGED":
            size, framerate = data
            _add_event("RTPVideoTransportRemoteFormatDidChange", dict(obj=self, size=size, framerate=framerate))
        elif name == "RECEIVED_KEYFRAME":
            _add_event("RTPVideoTransportReceivedKeyFrame", dict(obj=self))
        elif name == "MISSED_KEYFRAME":
            _add_event("RTPVideoTransportMissedKeyFrame", dict(obj=self))
        elif name == "REQUESTED_KEYFRAME":
            _add_event("RTPVideoTransportRequestedKeyFrame", dict(obj=self))


cdef class ICECandidate:
    def __init__(self, component, cand_type, address, port, priority, rel_addr=''):
        self.component = component
        self.type = cand_type
        self.address = address
        self.port = port
        self.priority = priority
        self.rel_address = rel_addr

    def __str__(self):
        return '(%s) %s:%d%s priority=%d type=%s' % (self.component,
                                                     self.address,
                                                     self.port,
                                                     ' rel_addr=%s' % self.rel_address if self.rel_address else '',
                                                     self.priority,
                                                     self.type.lower())

cdef ICECandidate ICECandidate_create(pj_ice_sess_cand *cand):
    cdef char buf[PJ_INET6_ADDRSTRLEN]
    cdef str address
    cdef str cand_type
    cdef int port

    if cand.type == PJ_ICE_CAND_TYPE_HOST:
        cand_type = 'HOST'
    elif cand.type == PJ_ICE_CAND_TYPE_SRFLX:
        cand_type = 'SRFLX'
    elif cand.type == PJ_ICE_CAND_TYPE_PRFLX:
        cand_type = 'PRFLX'
    elif cand.type == PJ_ICE_CAND_TYPE_RELAYED:
        cand_type = 'RELAY'
    else:
        cand_type = 'UNKNOWN'

    pj_sockaddr_print(&cand.addr, buf, PJ_INET6_ADDRSTRLEN, 0)
    address = _buf_to_str(buf)
    port = pj_sockaddr_get_port(&cand.addr)
    if pj_sockaddr_has_addr(&cand.rel_addr):
        pj_sockaddr_print(&cand.rel_addr, buf, PJ_INET6_ADDRSTRLEN, 0)
        rel_addr = _buf_to_str(buf)
    else:
        rel_addr = ''

    return ICECandidate('RTP' if cand.comp_id==1 else 'RTCP', cand_type, address, port, cand.prio, rel_addr)


cdef class ICECheck:
    def __init__(self, local_candidate, remote_candidate, state, nominated):
        self.local_candidate = local_candidate
        self.remote_candidate = remote_candidate
        self.state = state
        self.nominated = nominated

    def __str__(self):
        return '%s:%d -> %s:%d (%s, %s)' % (self.local_candidate.address, self.local_candidate.port,
                                            self.remote_candidate.address, self.remote_candidate.port,
                                            self.state.lower(), 'nominated' if self.nominated else 'not nominated')

cdef ICECheck ICECheck_create(pj_ice_sess_check *check):
    cdef str state
    cdef ICECandidate lcand
    cdef ICECandidate rcand

    if check.state == PJ_ICE_SESS_CHECK_STATE_FROZEN:
        state = 'FROZEN'
    elif check.state == PJ_ICE_SESS_CHECK_STATE_WAITING:
        state = 'WAITING'
    elif check.state == PJ_ICE_SESS_CHECK_STATE_IN_PROGRESS:
        state = 'IN_PROGRESS'
    elif check.state == PJ_ICE_SESS_CHECK_STATE_SUCCEEDED:
        state = 'SUCCEEDED'
    elif check.state == PJ_ICE_SESS_CHECK_STATE_FAILED:
        state = 'FAILED'
    else:
        state = 'UNKNOWN'

    lcand = ICECandidate_create(check.lcand)
    rcand = ICECandidate_create(check.rcand)

    return ICECheck(lcand, rcand, state, bool(check.nominated))

cdef ICECheck _get_rtp_valid_pair(pj_ice_strans *ice_st):
    cdef pj_ice_sess_check_ptr_const ice_check

    ice_check = pj_ice_strans_get_valid_pair(ice_st, 1)
    if ice_check == NULL:
        return None
    return ICECheck_create(<pj_ice_sess_check*>ice_check)


# helper functions

cdef dict _pj_math_stat_to_dict(pj_math_stat *stat):
    cdef dict retval = dict()
    retval["count"] = stat.n
    retval["max"] = stat.max
    retval["min"] = stat.min
    retval["last"] = stat.last
    retval["avg"] = stat.mean
    return retval

cdef dict _pjmedia_rtcp_stream_stat_to_dict(pjmedia_rtcp_stream_stat *stream_stat):
    cdef dict retval = dict()
    retval["packets"] = stream_stat.pkt
    retval["bytes"] = stream_stat.bytes
    retval["packets_discarded"] = stream_stat.discard
    retval["packets_lost"] = stream_stat.loss
    retval["packets_reordered"] = stream_stat.reorder
    retval["packets_duplicate"] = stream_stat.dup
    retval["loss_period"] = _pj_math_stat_to_dict(&stream_stat.loss_period)
    retval["burst_loss"] = bool(stream_stat.loss_type.burst)
    retval["random_loss"] = bool(stream_stat.loss_type.random)
    retval["jitter"] = _pj_math_stat_to_dict(&stream_stat.jitter)
    return retval

cdef str _ice_state_to_str(int state):
    if state == PJ_ICE_STRANS_STATE_NULL:
        return 'NULL'
    elif state == PJ_ICE_STRANS_STATE_INIT:
        return 'GATHERING'
    elif state == PJ_ICE_STRANS_STATE_READY:
        return 'GATHERING_COMPLETE'
    elif state == PJ_ICE_STRANS_STATE_SESS_READY:
        return 'NEGOTIATION_START'
    elif state == PJ_ICE_STRANS_STATE_NEGO:
        return 'NEGOTIATING'
    elif state == PJ_ICE_STRANS_STATE_RUNNING:
        return 'RUNNING'
    elif state == PJ_ICE_STRANS_STATE_FAILED:
        return 'FAILED'
    else:
        return 'UNKNOWN'

cdef dict _extract_ice_session_data(pj_ice_sess *ice_sess):
    cdef dict data = dict()
    cdef pj_ice_sess_cand *cand
    cdef pj_ice_sess_check *check

    # Process local candidates
    local_candidates = []
    for i in range(ice_sess.lcand_cnt):
        cand = &ice_sess.lcand[i]
        local_candidates.append(ICECandidate_create(cand))
    data['local_candidates'] = local_candidates

    # Process remote candidates
    remote_candidates = []
    for i in range(ice_sess.rcand_cnt):
        cand = &ice_sess.rcand[i]
        remote_candidates.append(ICECandidate_create(cand))
    data['remote_candidates'] = remote_candidates

    # Process valid pairs
    valid_pairs = []
    for i in range(ice_sess.comp_cnt):
        check = ice_sess.comp[i].valid_check
        valid_pairs.append(ICECheck_create(check))
    data['valid_pairs'] = valid_pairs

    # Process valid list
    valid_list = []
    for i in range(ice_sess.valid_list.count):
        check = &ice_sess.valid_list.checks[i]
        valid_list.append(ICECheck_create(check))
    data['valid_list'] = valid_list

    return data

cdef object _extract_rtp_transport(pjmedia_transport *tp):
    cdef void *rtp_transport_ptr = NULL

    if tp != NULL:
        rtp_transport_ptr = tp.user_data
    if rtp_transport_ptr == NULL:
        return None
    return (<object> rtp_transport_ptr)()

# callback functions

cdef void _RTPTransport_cb_ice_complete_impl(pjmedia_transport *tp, pj_ice_strans_op op, int status) with gil:
    # Despite the name this callback is not only called when ICE negotiation ends, it depends on the
    # op parameter
    cdef int lock_status
    cdef double duration
    cdef pj_ice_strans *ice_st
    cdef pj_ice_sess *ice_sess
    cdef pj_time_val tv, start_time
    cdef pj_mutex_t *lock
    cdef RTPTransport rtp_transport
    cdef PJSIPUA ua
    try:
        ua = _get_ua()
    except:
        return

    rtp_transport = _extract_rtp_transport(tp)
    if rtp_transport is None:
        return

    lock = rtp_transport._lock
    with nogil:
        lock_status = pj_mutex_lock(lock)
    if lock_status != 0:
        raise PJSIPError("failed to acquire lock", status)

    try:
        if op == PJ_ICE_STRANS_OP_NEGOTIATION:
            if status == 0:
                ice_st = pjmedia_ice_get_strans(tp)
                if ice_st == NULL:
                    return
                ice_sess = pj_ice_strans_get_session(ice_st)
                if ice_sess == NULL:
                    return
                start_time = pj_ice_strans_get_start_time(ice_st)
                pj_gettimeofday(&tv)
                tv.sec -= start_time.sec
                tv.msec -= start_time.msec
                pj_time_val_normalize(&tv)
                duration = (tv.sec*1000 + tv.msec)/1000.0
                data = _extract_ice_session_data(ice_sess)
                rtp_transport._rtp_valid_pair = _get_rtp_valid_pair(ice_st)
                _add_event("RTPTransportICENegotiationDidSucceed", dict(obj=rtp_transport,
                                                                        duration=duration,
                                                                        local_candidates=data['local_candidates'],
                                                                        remote_candidates=data['remote_candidates'],
                                                                        valid_pairs=data['valid_pairs'],
                                                                        valid_list=data['valid_list']))
            else:
                rtp_transport._rtp_valid_pair = None
                _add_event("RTPTransportICENegotiationDidFail", dict(obj=rtp_transport, reason=_pj_status_to_str(status)))
        elif op == PJ_ICE_STRANS_OP_INIT:
            if status == 0:
                rtp_transport.state = "INIT"
                _add_event("RTPTransportDidInitialize", dict(obj=rtp_transport))
            else:
                rtp_transport.state = "INVALID"
                _add_event("RTPTransportDidFail", dict(obj=rtp_transport, reason=_pj_status_to_str(status)))
        else:
            # silence compiler warning
            pass
    finally:
        with nogil:
            pj_mutex_unlock(lock)


cdef void _RTPTransport_cb_ice_complete(pjmedia_transport *tp, pj_ice_strans_op op, int status) noexcept nogil:
    with gil:
        _RTPTransport_cb_ice_complete_impl(tp, op, status)

cdef void _RTPTransport_cb_ice_state_impl(pjmedia_transport *tp, pj_ice_strans_state prev, pj_ice_strans_state curr) with gil:
    cdef int status
    cdef pj_mutex_t *lock
    cdef RTPTransport rtp_transport
    cdef PJSIPUA ua
    try:
        ua = _get_ua()
    except:
        return

    rtp_transport = _extract_rtp_transport(tp)
    if rtp_transport is None:
        return

    lock = rtp_transport._lock
    with nogil:
        status = pj_mutex_lock(lock)
    if status != 0:
        raise PJSIPError("failed to acquire lock", status)

    try:
        _add_event("RTPTransportICENegotiationStateDidChange", dict(obj=rtp_transport,
                                                                    prev_state=_ice_state_to_str(prev),
                                                                    state=_ice_state_to_str(curr)))
    finally:
        with nogil:
            pj_mutex_unlock(lock)


cdef void _RTPTransport_cb_ice_state(pjmedia_transport *tp, pj_ice_strans_state prev, pj_ice_strans_state curr) noexcept nogil:
    with gil:
        _RTPTransport_cb_ice_state_impl(tp, prev, curr)

cdef void _RTPTransport_cb_ice_stop_impl(pjmedia_transport *tp, char *reason, int err) with gil:
    cdef int status
    cdef pj_mutex_t *lock
    cdef RTPTransport rtp_transport
    cdef PJSIPUA ua
    try:
        ua = _get_ua()
    except:
        return

    rtp_transport = _extract_rtp_transport(tp)
    if rtp_transport is None:
        return

    lock = rtp_transport._lock
    with nogil:
        status = pj_mutex_lock(lock)
    if status != 0:
        raise PJSIPError("failed to acquire lock", status)

    try:
        rtp_transport._rtp_valid_pair = None
        _reason = reason
        if _reason != b"media stop requested":
            _add_event("RTPTransportICENegotiationDidFail", dict(obj=rtp_transport, reason=_reason))
    finally:
        with nogil:
            pj_mutex_unlock(lock)


cdef void _RTPTransport_cb_ice_stop(pjmedia_transport *tp, char *reason, int err) noexcept nogil:
    with gil:
        _RTPTransport_cb_ice_stop_impl(tp, reason, err)

cdef void _RTPTransport_cb_zrtp_secure_on_impl(pjmedia_transport *tp, char* cipher) with gil:
   cdef RTPTransport rtp_transport
   cdef PJSIPUA ua
   try:
       ua = _get_ua()
   except:
       return
   try:
       rtp_transport = _extract_rtp_transport(tp)
       if rtp_transport is None:
           return
       _add_event("RTPTransportZRTPSecureOn", dict(obj=rtp_transport, cipher=bytes(cipher)))
   except:
       ua._handle_exception(1)

cdef void _RTPTransport_cb_zrtp_secure_on(pjmedia_transport *tp, char* cipher) noexcept nogil:
    with gil:
        _RTPTransport_cb_zrtp_secure_on_impl(tp, cipher)

cdef void _RTPTransport_cb_zrtp_secure_off_impl(pjmedia_transport *tp) with gil:
   cdef RTPTransport rtp_transport
   cdef PJSIPUA ua
   try:
       ua = _get_ua()
   except:
       return
   try:
       rtp_transport = _extract_rtp_transport(tp)
       if rtp_transport is None:
           return
       _add_event("RTPTransportZRTPSecureOff", dict(obj=rtp_transport))
   except:
       ua._handle_exception(1)

cdef void _RTPTransport_cb_zrtp_secure_off(pjmedia_transport *tp) noexcept nogil:
    with gil:
        _RTPTransport_cb_zrtp_secure_off_impl(tp)

cdef void _RTPTransport_cb_zrtp_show_sas_impl(pjmedia_transport *tp, char* sas, int verified) with gil:
   cdef RTPTransport rtp_transport
   cdef PJSIPUA ua
   try:
       ua = _get_ua()
   except:
       return
   try:
       rtp_transport = _extract_rtp_transport(tp)
       if rtp_transport is None:
           return
       _add_event("RTPTransportZRTPReceivedSAS", dict(obj=rtp_transport, sas=bytes(sas), verified=bool(verified)))
   except:
       ua._handle_exception(1)

cdef void _RTPTransport_cb_zrtp_show_sas(pjmedia_transport *tp, char* sas, int verified) noexcept nogil:
    with gil:
        _RTPTransport_cb_zrtp_show_sas_impl(tp, sas, verified)

cdef void _RTPTransport_cb_zrtp_confirm_goclear_impl(pjmedia_transport *tp) with gil:
   cdef RTPTransport rtp_transport
   cdef PJSIPUA ua
   try:
       ua = _get_ua()
   except:
       return
   try:
       rtp_transport = _extract_rtp_transport(tp)
       if rtp_transport is None:
           return
       # TODO: not yet implemented by PJSIP's ZRTP transport
   except:
       ua._handle_exception(1)

cdef void _RTPTransport_cb_zrtp_confirm_goclear(pjmedia_transport *tp) noexcept nogil:
    with gil:
        _RTPTransport_cb_zrtp_confirm_goclear_impl(tp)

cdef void _RTPTransport_cb_zrtp_show_message_impl(pjmedia_transport *tp, int severity, int sub_code) with gil:
    global zrtp_message_levels, zrtp_error_messages
    cdef RTPTransport rtp_transport
    cdef PJSIPUA ua
    try:
        ua = _get_ua()
    except:
        return
    try:
        rtp_transport = _extract_rtp_transport(tp)
        if rtp_transport is None:
            return
        level = zrtp_message_levels.get(severity, 1)
        if severity == 2 and sub_code in (5, 6, 7):
            pass
        else:
            message = zrtp_error_messages[level].get(sub_code, 'Unknown')
            _add_event("RTPTransportZRTPLog", dict(obj=rtp_transport, level=level, message=message))
    except:
        ua._handle_exception(1)

cdef void _RTPTransport_cb_zrtp_show_message(pjmedia_transport *tp, int severity, int sub_code) noexcept nogil:
    with gil:
        _RTPTransport_cb_zrtp_show_message_impl(tp, severity, sub_code)

cdef void _RTPTransport_cb_zrtp_negotiation_failed_impl(pjmedia_transport *tp, int severity, int sub_code) with gil:
    global zrtp_message_levels, zrtp_error_messages
    cdef RTPTransport rtp_transport
    cdef PJSIPUA ua
    try:
        ua = _get_ua()
    except:
        return
    try:
        rtp_transport = _extract_rtp_transport(tp)
        if rtp_transport is None:
            return
        level = zrtp_message_levels.get(severity, 1)
        reason = zrtp_error_messages[level].get(sub_code, 'Unknown')
        _add_event("RTPTransportZRTPNegotiationFailed", dict(obj=rtp_transport, reason=reason))
    except:
        ua._handle_exception(1)

cdef void _RTPTransport_cb_zrtp_negotiation_failed(pjmedia_transport *tp, int severity, int sub_code) noexcept nogil:
    with gil:
        _RTPTransport_cb_zrtp_negotiation_failed_impl(tp, severity, sub_code)

cdef void _RTPTransport_cb_zrtp_not_supported_by_other_impl(pjmedia_transport *tp) with gil:
    cdef RTPTransport rtp_transport
    cdef PJSIPUA ua
    try:
        ua = _get_ua()
    except:
        return
    try:
        rtp_transport = _extract_rtp_transport(tp)
        if rtp_transport is None:
            return
        _add_event("RTPTransportZRTPNotSupportedByRemote", dict(obj=rtp_transport))
    except:
        ua._handle_exception(1)

cdef void _RTPTransport_cb_zrtp_not_supported_by_other(pjmedia_transport *tp) noexcept nogil:
    with gil:
        _RTPTransport_cb_zrtp_not_supported_by_other_impl(tp)

cdef void _RTPTransport_cb_zrtp_ask_enrollment_impl(pjmedia_transport *tp, int info) with gil:
    cdef RTPTransport rtp_transport
    cdef PJSIPUA ua
    try:
        ua = _get_ua()
    except:
        return
    try:
        rtp_transport = _extract_rtp_transport(tp)
        if rtp_transport is None:
            return
        # TODO: implement PBX enrollment
    except:
        ua._handle_exception(1)

cdef void _RTPTransport_cb_zrtp_ask_enrollment(pjmedia_transport *tp, int info) noexcept nogil:
    with gil:
        _RTPTransport_cb_zrtp_ask_enrollment_impl(tp, info)

cdef void _RTPTransport_cb_zrtp_inform_enrollment_impl(pjmedia_transport *tp, int info) with gil:
    cdef RTPTransport rtp_transport
    cdef PJSIPUA ua
    try:
        ua = _get_ua()
    except:
        return
    try:
        rtp_transport = _extract_rtp_transport(tp)
        if rtp_transport is None:
            return
        # TODO: implement PBX enrollment
    except:
        ua._handle_exception(1)

cdef void _RTPTransport_cb_zrtp_inform_enrollment(pjmedia_transport *tp, int info) noexcept nogil:
    with gil:
        _RTPTransport_cb_zrtp_inform_enrollment_impl(tp, info)

cdef void _AudioTransport_cb_dtmf_impl(pjmedia_stream *stream, void *user_data, int digit) with gil:
    cdef AudioTransport audio_stream = (<object> user_data)()
    cdef PJSIPUA ua
    try:
        ua = _get_ua()
    except:
        return
    if audio_stream is None:
        return
    try:
        _add_event("RTPAudioStreamGotDTMF", dict(obj=audio_stream, digit=chr(digit)))
    except:
        ua._handle_exception(1)

cdef void _AudioTransport_cb_dtmf(pjmedia_stream *stream, void *user_data, int digit) noexcept nogil:
    with gil:
        _AudioTransport_cb_dtmf_impl(stream, user_data, digit)

# globals

cdef pjmedia_ice_cb _ice_cb
_ice_cb.on_ice_complete = _RTPTransport_cb_ice_complete
_ice_cb.on_ice_state = _RTPTransport_cb_ice_state
_ice_cb.on_ice_stop = _RTPTransport_cb_ice_stop

valid_sdp_directions = (b"sendrecv", b"sendonly", b"recvonly", b"inactive")

# ZRTP

cdef pjmedia_zrtp_cb _zrtp_cb
_zrtp_cb.secure_on = _RTPTransport_cb_zrtp_secure_on
_zrtp_cb.secure_off = _RTPTransport_cb_zrtp_secure_off
_zrtp_cb.show_sas = _RTPTransport_cb_zrtp_show_sas
_zrtp_cb.confirm_go_clear = _RTPTransport_cb_zrtp_confirm_goclear
_zrtp_cb.show_message = _RTPTransport_cb_zrtp_show_message
_zrtp_cb.negotiation_failed = _RTPTransport_cb_zrtp_negotiation_failed
_zrtp_cb.not_supported_by_other = _RTPTransport_cb_zrtp_not_supported_by_other
_zrtp_cb.ask_enrollment = _RTPTransport_cb_zrtp_ask_enrollment
_zrtp_cb.inform_enrollment = _RTPTransport_cb_zrtp_inform_enrollment
_zrtp_cb.sign_sas = NULL
_zrtp_cb.check_sas_signature = NULL

# Keep these aligned with ZrtpCodes.h

cdef dict zrtp_message_levels = {1: 'INFO', 2: 'WARNING', 3: 'SEVERE', 4: 'ERROR'}
cdef dict zrtp_error_messages = {
    'INFO': {
        0: "Unknown",
        1: "Hello received and prepared a Commit, ready to get peer's hello hash", #InfoHelloReceived
        2: "Commit: Generated a public DH key",                                    #InfoCommitDHGenerated
        3: "Responder: Commit received, preparing DHPart1",                        #InfoRespCommitReceived
        4: "DH1Part: Generated a public DH key",                                   #InfoDH1DHGenerated
        5: "Initiator: DHPart1 received, preparing DHPart2",                       #InfoInitDH1Received
        6: "Responder: DHPart2 received, preparing Confirm1",                      #InfoRespDH2Received
        7: "Initiator: Confirm1 received, preparing Confirm2",                     #InfoInitConf1Received
        8: "Responder: Confirm2 received, preparing Conf2Ack",                     #InfoRespConf2Received
        9: "At least one retained secrets matches - security OK",                  #InfoRSMatchFound
       10: "Entered secure state",                                                 #InfoSecureStateOn
       11: "No more security for this session",                                    #InfoSecureStateOff
    },
    'WARNING': {
        0: "Unknown",
        1: "WarningDHAESmismatch = 1, //!< Commit contains an AES256 cipher but does not offer a Diffie-Helman 4096 - not used DH4096 was discarded", #WarningDHAESmismatch
        2: "Received a GoClear message",                                                                                                              #WarningGoClearReceived
        3: "Hello offers an AES256 cipher but does not offer a Diffie-Helman 4096- not used DH4096 was discarded",                                    #WarningDHShort
        4: "No retained shared secrets available - must verify SAS",                                                                                  #WarningNoRSMatch
        5: "Internal ZRTP packet checksum mismatch - packet dropped",                                                                                 #WarningCRCmismatch
        6: "Dropping packet because SRTP authentication failed!",                                                                                     #WarningSRTPauthError
        7: "Dropping packet because SRTP replay check failed!",                                                                                       #WarningSRTPreplayError
        8: "Valid retained shared secrets availabe but no matches found - must verify SAS",                                                           #WarningNoExpectedRSMatch
    },
    'SEVERE': {
        0: "Unknown",
        1: "Hash HMAC check of Hello failed!",                                    #SevereHelloHMACFailed
        2: "Hash HMAC check of Commit failed!",                                   #SevereCommitHMACFailed
        3: "Hash HMAC check of DHPart1 failed!",                                  #SevereDH1HMACFailed
        4: "Hash HMAC check of DHPart2 failed!",                                  #SevereDH2HMACFailed
        5: "Cannot send data - connection or peer down?",                         #SevereCannotSend
        6: "Internal protocol error occured!",                                    #SevereProtocolError
        7: "Cannot start a timer - internal resources exhausted?",                #SevereNoTimer
        8: "Too much retries during ZRTP negotiation - connection or peer down?", #SevereTooMuchRetries
    },
    'ERROR': {
              0x00: "Unknown",
              0x10: "Malformed packet (CRC OK, but wrong structure)", #MalformedPacket
              0x20: "Critical software error",                        #CriticalSWError
              0x30: "Unsupported ZRTP version",                       #UnsuppZRTPVersion
              0x40: "Hello components mismatch",                      #HelloCompMismatch
              0x51: "Hash type not supported",                        #UnsuppHashType
              0x52: "Cipher type not supported",                      #UnsuppCiphertype
              0x53: "Public key exchange not supported",              #UnsuppPKExchange
              0x54: "SRTP auth. tag not supported",                   #UnsuppSRTPAuthTag
              0x55: "SAS scheme not supported",                       #UnsuppSASScheme
              0x56: "No shared secret available, DH mode required",   #NoSharedSecret
              0x61: "DH Error: bad pvi or pvr ( == 1, 0, or p-1)",    #DHErrorWrongPV
              0x62: "DH Error: hvi != hashed data",                   #DHErrorWrongHVI
              0x63: "Received relayed SAS from untrusted MiTM",       #SASuntrustedMiTM
              0x70: "Auth. Error: Bad Confirm pkt HMAC",              #ConfirmHMACWrong
              0x80: "Nonce reuse",                                    #NonceReused
              0x90: "Equal ZIDs in Hello",                            #EqualZIDHello
             0x100: "GoClear packet received, but not allowed",       #GoCleatNotAllowed
        0x7fffffff: "Packet ignored",                                 #IgnorePacket
    }
}

