mirror of
https://github.com/servalproject/serval-dna.git
synced 2025-02-21 01:42:18 +00:00
Pass phone numbers in vomp call initiation
This commit is contained in:
parent
2bd26dfcdb
commit
3ab79d8e5b
@ -1790,18 +1790,6 @@ command_line_option command_line_options[]={
|
||||
"Create a new identity in the keyring protected by the provided PIN"},
|
||||
{app_keyring_set_did,{"set","did","<sid>","<did>","<name>","[<pin>]",NULL},CLIFLAG_STANDALONE,
|
||||
"Set the DID for the specified SID. Optionally supply PIN to unlock the SID record in the keyring."},
|
||||
{app_vomp_status,{"vomp","status",NULL},0,
|
||||
"Display status of any VoMP calls"},
|
||||
{app_vomp_monitor,{"vomp","monitor",NULL},0,
|
||||
"Monitor state and audio-flow of VoMP calls"},
|
||||
{app_vomp_pickup,{"vomp","pickup","<call>",NULL},0,
|
||||
"Accept specified call (use vomp status to get list of calls)"},
|
||||
{app_vomp_hangup,{"vomp","hangup","<call>",NULL},0,
|
||||
"End specified call (use vomp status to get list of calls)"},
|
||||
{app_vomp_dtmf,{"vomp","dtmf","<call>","<digits>",NULL},0,
|
||||
"Send DTMF digits over specified call"},
|
||||
{app_vomp_dial,{"vomp","dial","<sid>","<did>","[<callerid>]",NULL},0,
|
||||
"Attempt to dial the specified sid and did."},
|
||||
{app_id_self,{"id","self",NULL},0,
|
||||
"Return my own identity(s) as URIs"},
|
||||
{app_id_self,{"id","peers",NULL},0,
|
||||
|
17
constants.h
17
constants.h
@ -297,7 +297,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
Multiple replies can be used to respond with more. */
|
||||
#define MDP_MAX_SID_REQUEST 59
|
||||
|
||||
#define MDP_VOMPEVENT 7
|
||||
#define VOMP_MAX_CALLS 16
|
||||
/* Maximum amount of audio to cram into a VoMP audio packet.
|
||||
More lets us include preemptive retransmissions.
|
||||
@ -305,22 +304,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
the bandwidth used. */
|
||||
#define VOMP_STUFF_BYTES 800
|
||||
|
||||
/* For overlay_mdp_vompevent->flags */
|
||||
#define VOMPEVENT_RINGING (1<<0)
|
||||
#define VOMPEVENT_CALLENDED (1<<1)
|
||||
#define VOMPEVENT_CALLREJECT (1<<2)
|
||||
#define VOMPEVENT_HANGUP VOMPEVENT_CALLREJECT
|
||||
#define VOMPEVENT_TIMEOUT (1<<3)
|
||||
#define VOMPEVENT_ERROR (1<<4)
|
||||
#define VOMPEVENT_AUDIOSTREAMING (1<<5)
|
||||
#define VOMPEVENT_DIAL (1<<6)
|
||||
#define VOMPEVENT_REGISTERINTEREST (1<<7)
|
||||
#define VOMPEVENT_WITHDRAWINTEREST (1<<8)
|
||||
#define VOMPEVENT_CALLCREATED (1<<9)
|
||||
#define VOMPEVENT_PICKUP (1<<10)
|
||||
#define VOMPEVENT_CALLINFO (1<<11)
|
||||
#define VOMPEVENT_AUDIOPACKET (1<<12)
|
||||
|
||||
#define MAX_AUDIO_BYTES 1024
|
||||
#define MDP_NODEINFO 8
|
||||
#define MDP_GOODBYE 9
|
||||
|
@ -409,7 +409,6 @@ int monitor_process_command(struct monitor_context *c)
|
||||
else if (sscanf(cmd,"lookup match %s %d %s %s",sid,&port,localDid,remoteDid)>=3) {
|
||||
monitor_send_lookup_response(sid,port,localDid,remoteDid);
|
||||
}else if (sscanf(cmd,"call %s %s %s",sid,localDid,remoteDid)==3) {
|
||||
DEBUG("here");
|
||||
int gotSid = 0;
|
||||
if (sid[0]=='*') {
|
||||
/* For testing, pick any peer and call them */
|
||||
|
@ -948,10 +948,6 @@ void overlay_mdp_poll(struct sched_ent *alarm)
|
||||
if (debug & DEBUG_MDPREQUESTS) DEBUG("MDP_GOODBYE");
|
||||
overlay_mdp_releasebindings(recvaddr_un,recvaddrlen);
|
||||
return;
|
||||
case MDP_VOMPEVENT:
|
||||
if (debug & DEBUG_MDPREQUESTS) DEBUG("MDP_VOMPEVENT");
|
||||
vomp_mdp_event(mdp,recvaddr_un,recvaddrlen);
|
||||
return;
|
||||
case MDP_NODEINFO:
|
||||
if (debug & DEBUG_MDPREQUESTS) DEBUG("MDP_NODEINFO");
|
||||
overlay_route_node_info(mdp,recvaddr_un,recvaddrlen);
|
||||
@ -1103,10 +1099,6 @@ int overlay_mdp_relevant_bytes(overlay_mdp_frame *mdp)
|
||||
len+=strlen(mdp->error.message)+1;
|
||||
if (mdp->error.error) INFOF("mdp return/error code: %d:%s",mdp->error.error,mdp->error.message);
|
||||
break;
|
||||
case MDP_VOMPEVENT:
|
||||
/* XXX too hard to work out precisely for now. */
|
||||
len=sizeof(overlay_mdp_frame);
|
||||
break;
|
||||
case MDP_NODEINFO:
|
||||
/* XXX problems with calculating this due to structure padding,
|
||||
so doubled required space, and now it works. */
|
||||
|
19
serval.h
19
serval.h
@ -1009,17 +1009,6 @@ void _serval_debug_free(void *p,char *file,const char *func,int line);
|
||||
#endif
|
||||
|
||||
|
||||
typedef struct vomp_call_half {
|
||||
unsigned char sid[SID_SIZE];
|
||||
unsigned char did[64];
|
||||
unsigned char state;
|
||||
unsigned char codec;
|
||||
unsigned int session;
|
||||
unsigned int sequence;
|
||||
/* the following is from call creation, not start of audio flow */
|
||||
unsigned long long milliseconds_since_call_start;
|
||||
} vomp_call_half;
|
||||
|
||||
typedef struct vomp_sample_block {
|
||||
unsigned int codec;
|
||||
time_ms_t starttime;
|
||||
@ -1029,8 +1018,6 @@ typedef struct vomp_sample_block {
|
||||
|
||||
struct vomp_call_state;
|
||||
struct vomp_call_state *vomp_find_call_by_session(int session_token);
|
||||
int vomp_mdp_event(overlay_mdp_frame *mdp,
|
||||
struct sockaddr_un *recvaddr,int recvaddrlen);
|
||||
int vomp_mdp_received(overlay_mdp_frame *mdp);
|
||||
int vomp_tick_interval();
|
||||
int vomp_sample_size(int c);
|
||||
@ -1073,15 +1060,9 @@ int overlay_broadcast_ensemble(int interface_number,
|
||||
struct sockaddr_in *recipientaddr /* NULL == broadcast */,
|
||||
unsigned char *bytes,int len);
|
||||
|
||||
int app_vomp_status(int argc, const char *const *argv, struct command_line_option *o);
|
||||
int app_vomp_dial(int argc, const char *const *argv, struct command_line_option *o);
|
||||
int app_vomp_pickup(int argc, const char *const *argv, struct command_line_option *o);
|
||||
int app_vomp_hangup(int argc, const char *const *argv, struct command_line_option *o);
|
||||
int app_vomp_monitor(int argc, const char *const *argv, struct command_line_option *o);
|
||||
#ifdef HAVE_VOIPTEST
|
||||
int app_pa_phone(int argc, const char *const *argv, struct command_line_option *o);
|
||||
#endif
|
||||
int app_vomp_dtmf(int argc, const char *const *argv, struct command_line_option *o);
|
||||
int app_monitor_cli(int argc, const char *const *argv, struct command_line_option *o);
|
||||
|
||||
int monitor_get_fds(struct pollfd *fds,int *fdcount,int fdmax);
|
||||
|
664
vomp.c
664
vomp.c
@ -90,10 +90,21 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#define VOMP_STATE_INCALL 5
|
||||
#define VOMP_STATE_CALLENDED 6
|
||||
|
||||
struct vomp_call_half {
|
||||
unsigned char sid[SID_SIZE];
|
||||
char did[64];
|
||||
unsigned char state;
|
||||
unsigned char codec;
|
||||
unsigned int session;
|
||||
unsigned int sequence;
|
||||
/* the following is from call creation, not start of audio flow */
|
||||
unsigned long long milliseconds_since_call_start;
|
||||
};
|
||||
|
||||
struct vomp_call_state {
|
||||
struct sched_ent alarm;
|
||||
vomp_call_half local;
|
||||
vomp_call_half remote;
|
||||
struct vomp_call_half local;
|
||||
struct vomp_call_half remote;
|
||||
int initiated_call;
|
||||
int fast_audio;
|
||||
time_ms_t create_time;
|
||||
@ -120,20 +131,12 @@ struct vomp_call_state vomp_call_states[VOMP_MAX_CALLS];
|
||||
struct profile_total vomp_stats;
|
||||
|
||||
static void vomp_process_tick(struct sched_ent *alarm);
|
||||
static int dump_vomp_status();
|
||||
static const char *vomp_describe_codec(int c);
|
||||
strbuf strbuf_append_vomp_supported_codecs(strbuf sb, const unsigned char supported_codecs[256]);
|
||||
|
||||
/* which codecs we support (set by registered listener) */
|
||||
unsigned char vomp_local_codec_list[256];
|
||||
|
||||
/* Now keep track of who wants to know what we are up to */
|
||||
int vomp_interested_usock_count=0;
|
||||
#define VOMP_MAX_INTERESTED 128
|
||||
struct sockaddr_un *vomp_interested_usocks[VOMP_MAX_INTERESTED];
|
||||
int vomp_interested_usock_lengths[VOMP_MAX_INTERESTED];
|
||||
time_ms_t vomp_interested_expiries[VOMP_MAX_INTERESTED];
|
||||
|
||||
struct vomp_call_state *vomp_find_call_by_session(int session_token)
|
||||
{
|
||||
int i;
|
||||
@ -274,6 +277,7 @@ struct vomp_call_state *vomp_find_or_create_call(unsigned char *remote_sid,
|
||||
int vomp_send_status_remote_audio(struct vomp_call_state *call, int audio_codec, const unsigned char *audio, int audio_length)
|
||||
{
|
||||
overlay_mdp_frame mdp;
|
||||
unsigned short *len=&mdp.out.payload_length;
|
||||
|
||||
bzero(&mdp,sizeof(mdp));
|
||||
mdp.packetTypeAndFlags=MDP_TX;
|
||||
@ -298,24 +302,34 @@ int vomp_send_status_remote_audio(struct vomp_call_state *call, int audio_codec,
|
||||
mdp.out.payload[12]=(call->local.session>>8)&0xff;
|
||||
mdp.out.payload[13]=(call->local.session>>0)&0xff;
|
||||
|
||||
mdp.out.payload_length=14;
|
||||
*len=14;
|
||||
|
||||
if (call->local.state < VOMP_STATE_RINGINGOUT && call->remote.state < VOMP_STATE_RINGINGOUT) {
|
||||
/* Also include list of supported codecs */
|
||||
/* Include src and dst phone numbers */
|
||||
int didLen;
|
||||
|
||||
/* Include the list of supported codecs */
|
||||
int i;
|
||||
for (i = 0; i < 256; ++i)
|
||||
if (vomp_local_codec_list[i]) {
|
||||
mdp.out.payload[mdp.out.payload_length++]=i;
|
||||
mdp.out.payload[(*len)++]=i;
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUGF("I support the %s codec", vomp_describe_codec(i));
|
||||
}
|
||||
mdp.out.payload[mdp.out.payload_length++]=0;
|
||||
mdp.out.payload[(*len)++]=0;
|
||||
|
||||
if (call->initiated_call){
|
||||
didLen = snprintf((char *)(mdp.out.payload + *len), sizeof(mdp.out.payload) - *len, "%s",call->local.did);
|
||||
*len+=didLen;
|
||||
didLen = snprintf((char *)(mdp.out.payload + *len), sizeof(mdp.out.payload) - *len, "%s", call->remote.did);
|
||||
*len+=didLen;
|
||||
}
|
||||
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUGF("mdp frame with codec list is %d bytes", mdp.out.payload_length);
|
||||
}
|
||||
|
||||
if (call->local.state==VOMP_STATE_INCALL && audio && audio_length && vomp_sample_size(audio_codec)==audio_length) {
|
||||
unsigned short *len=&mdp.out.payload_length;
|
||||
unsigned char *p=&mdp.out.payload[0];
|
||||
|
||||
// DEBUG("Including audio sample block");
|
||||
@ -386,60 +400,6 @@ int vomp_send_status_remote(struct vomp_call_state *call)
|
||||
return vomp_send_status_remote_audio(call, 0, NULL, 0);
|
||||
}
|
||||
|
||||
int vomp_send_mdp_status_audio(struct vomp_call_state *call, int audio_codec, unsigned int start_time, unsigned int end_time, const unsigned char *audio, int audio_length)
|
||||
{
|
||||
if (audio && audio_length && vomp_sample_size(audio_codec)!=audio_length)
|
||||
return WHY("Audio frame is the wrong length");
|
||||
|
||||
overlay_mdp_frame mdp;
|
||||
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUG("Sending mdp client packet");
|
||||
|
||||
bzero(&mdp,sizeof(mdp));
|
||||
mdp.packetTypeAndFlags=MDP_VOMPEVENT;
|
||||
mdp.vompevent.call_session_token=call->local.session;
|
||||
mdp.vompevent.last_activity=call->last_activity;
|
||||
if (call->local.state==VOMP_STATE_CALLENDED)
|
||||
mdp.vompevent.flags|=VOMPEVENT_CALLENDED;
|
||||
if (call->remote.state==VOMPEVENT_CALLENDED)
|
||||
mdp.vompevent.flags|=VOMPEVENT_CALLREJECT;
|
||||
if (call->audio_started)
|
||||
mdp.vompevent.flags|=VOMPEVENT_AUDIOSTREAMING;
|
||||
// TODO ???
|
||||
//mdp.vompevent.flags|=VOMPEVENT_CALLCREATED;
|
||||
mdp.vompevent.local_state=call->local.state;
|
||||
mdp.vompevent.remote_state=call->remote.state;
|
||||
|
||||
bcopy(&call->remote_codec_list[0],&mdp.vompevent.supported_codecs[0],256);
|
||||
|
||||
if (audio && audio_length) {
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUGF("Frame contains audio (codec=%s)", vomp_describe_codec(audio_codec));
|
||||
bcopy(audio, &mdp.vompevent.audio_bytes[0], audio_length);
|
||||
mdp.vompevent.audio_sample_codec=audio_codec;
|
||||
mdp.vompevent.audio_sample_bytes=audio_length;
|
||||
mdp.vompevent.audio_sample_starttime=start_time;
|
||||
mdp.vompevent.audio_sample_endtime=end_time;
|
||||
}
|
||||
|
||||
int i;
|
||||
time_ms_t now = gettime_ms();
|
||||
for(i=0;i<vomp_interested_usock_count;i++)
|
||||
if (vomp_interested_expiries[i]>=now) {
|
||||
overlay_mdp_reply(mdp_named.poll.fd,
|
||||
vomp_interested_usocks[i],
|
||||
vomp_interested_usock_lengths[i],
|
||||
&mdp);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vomp_send_mdp_status(struct vomp_call_state *call)
|
||||
{
|
||||
return vomp_send_mdp_status_audio(call,0,0,0,NULL,0);
|
||||
}
|
||||
|
||||
int monitor_call_status(struct vomp_call_state *call)
|
||||
{
|
||||
char msg[1024];
|
||||
@ -546,10 +506,6 @@ int vomp_update(struct vomp_call_state *call)
|
||||
if (monitor_socket_count && monitor_client_interested(MONITOR_VOMP))
|
||||
monitor_call_status(call);
|
||||
|
||||
// tell mdp clients
|
||||
if (vomp_interested_usock_count)
|
||||
vomp_send_mdp_status(call);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -604,12 +560,6 @@ int vomp_process_audio(struct vomp_call_state *call,unsigned int sender_duration
|
||||
|
||||
/* Pass audio frame to all registered listeners */
|
||||
if (!vomp_audio_already_seen(call, e)){
|
||||
if (vomp_interested_usock_count)
|
||||
vomp_send_mdp_status_audio(call, codec, s, e,
|
||||
&mdp->in.payload[ofs+1],
|
||||
vomp_sample_size(codec)
|
||||
);
|
||||
|
||||
if (monitor_socket_count)
|
||||
monitor_send_audio(call, codec, s, e,
|
||||
&mdp->in.payload[ofs+1],
|
||||
@ -686,6 +636,11 @@ int vomp_dial(unsigned char *local_sid, unsigned char *remote_sid, char *local_d
|
||||
local_sid,
|
||||
0,
|
||||
0);
|
||||
|
||||
/* Copy local / remote phone numbers */
|
||||
strlcpy(call->local.did, local_did, sizeof(call->local.did));
|
||||
strlcpy(call->remote.did, remote_did, sizeof(call->remote.did));
|
||||
|
||||
vomp_update_local_state(call, VOMP_STATE_CALLPREP);
|
||||
// remember that we initiated this call, not the other party
|
||||
call->initiated_call = 1;
|
||||
@ -725,239 +680,23 @@ int vomp_hangup(struct vomp_call_state *call)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* An MDP message of type MDP_VOMPEVENT received from the unix domain socket.
|
||||
This is how user tasks request telephone calls and receive updated status
|
||||
and audio from the call. We need the receiver socket so that we can
|
||||
route the events back to wherever they should be going.
|
||||
XXX - We should have some means of authenticating/protecting this interface
|
||||
so that any old process cannot request a mesh call. Although, in fairness,
|
||||
the user will know about the call because the call display will come up.
|
||||
*/
|
||||
|
||||
int vomp_mdp_event(overlay_mdp_frame *mdp, struct sockaddr_un *recvaddr,int recvaddrlen)
|
||||
{
|
||||
/* Frames from the user can take only a few forms:
|
||||
- announce interest in call state.
|
||||
- withdraw interest in call state.
|
||||
- place a call (SID+DID combination)
|
||||
- deliver audio for sending
|
||||
- indicate pickup, hangup or call reject
|
||||
|
||||
We then send back all sorts of relevant call state information as well as
|
||||
transported audio. In particular we inform when the call state changes,
|
||||
including if any error has occurred.
|
||||
*/
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUGF("Flags=0x%x",mdp->vompevent.flags);
|
||||
|
||||
switch(mdp->vompevent.flags)
|
||||
{
|
||||
case VOMPEVENT_REGISTERINTEREST:
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUG("Request to register interest");
|
||||
/* put unix domain socket on record to send call state event and audio to. */
|
||||
{
|
||||
int i;
|
||||
int candidate=-1;
|
||||
time_ms_t now = gettime_ms();
|
||||
for(i=0;i<vomp_interested_usock_count;i++)
|
||||
{
|
||||
if (vomp_interested_usock_lengths[i]==recvaddrlen)
|
||||
if (!memcmp(recvaddr->sun_path,
|
||||
vomp_interested_usocks[i],recvaddrlen))
|
||||
/* found it -- so we are already monitoring this one */
|
||||
return overlay_mdp_reply_error(mdp_named.poll.fd,recvaddr,recvaddrlen,
|
||||
0,"Success");
|
||||
if (vomp_interested_expiries[i]<now) candidate=i;
|
||||
}
|
||||
if (i>=vomp_interested_usock_count&&(candidate>-1)) i=candidate;
|
||||
/* not currently on the list, so add */
|
||||
if (i<VOMP_MAX_INTERESTED) {
|
||||
if (vomp_interested_usocks[i]) {
|
||||
free(vomp_interested_usocks[i]);
|
||||
vomp_interested_usocks[i]=NULL;
|
||||
}
|
||||
vomp_interested_usocks[i]=malloc(recvaddrlen);
|
||||
if (!vomp_interested_usocks[i])
|
||||
return overlay_mdp_reply_error(mdp_named.poll.fd, recvaddr,recvaddrlen,
|
||||
4002,"Out of memory");
|
||||
bcopy(recvaddr,vomp_interested_usocks[i],
|
||||
recvaddrlen);
|
||||
vomp_interested_usock_lengths[i]=recvaddrlen;
|
||||
vomp_interested_expiries[i] = gettime_ms() + 60000;
|
||||
if (i==vomp_interested_usock_count) vomp_interested_usock_count++;
|
||||
|
||||
if (mdp->vompevent.supported_codecs[0]) {
|
||||
/* Replace set of locally supported codecs */
|
||||
for(i=0;i<256;i++) vomp_local_codec_list[i]=0;
|
||||
for(i=0;(i<256)&&mdp->vompevent.supported_codecs[i];i++)
|
||||
{
|
||||
vomp_local_codec_list[mdp->vompevent.supported_codecs[i]]=1;
|
||||
}
|
||||
}
|
||||
|
||||
return overlay_mdp_reply_error
|
||||
(mdp_named.poll.fd,recvaddr,recvaddrlen,0,"Success");
|
||||
} else {
|
||||
return overlay_mdp_reply_error
|
||||
(mdp_named.poll.fd,recvaddr,recvaddrlen,
|
||||
4003,"Too many listeners (try again in a minute?)");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case VOMPEVENT_WITHDRAWINTEREST:
|
||||
/* opposite of above */
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUG("Request to withdraw interest");
|
||||
{
|
||||
int i;
|
||||
for(i=0;i<vomp_interested_usock_count;i++)
|
||||
{
|
||||
if (vomp_interested_usock_lengths[i]==recvaddrlen)
|
||||
if (!memcmp(recvaddr->sun_path,
|
||||
vomp_interested_usocks[i],recvaddrlen))
|
||||
{
|
||||
/* found it -- so we are already monitoring this one */
|
||||
free(vomp_interested_usocks[i]);
|
||||
if (i<vomp_interested_usock_count-1)
|
||||
{
|
||||
int swap=vomp_interested_usock_count-1;
|
||||
vomp_interested_usock_lengths[i]
|
||||
=vomp_interested_usock_lengths[swap];
|
||||
vomp_interested_usocks[i]=vomp_interested_usocks[swap];
|
||||
vomp_interested_expiries[i] = vomp_interested_expiries[swap];
|
||||
}
|
||||
vomp_interested_usock_count--;
|
||||
return overlay_mdp_reply_error
|
||||
(mdp_named.poll.fd,recvaddr,recvaddrlen,
|
||||
0,"Success. You have been removed.");
|
||||
}
|
||||
}
|
||||
return overlay_mdp_reply_error
|
||||
(mdp_named.poll.fd,recvaddr,recvaddrlen,
|
||||
0,"Success. You were never listening.");
|
||||
}
|
||||
break;
|
||||
case VOMPEVENT_CALLINFO:
|
||||
{
|
||||
/* provide call endpoint info to user */
|
||||
struct vomp_call_state *call
|
||||
=vomp_find_call_by_session(mdp->vompevent.call_session_token);
|
||||
|
||||
/* collect call info and send to requestor */
|
||||
overlay_mdp_frame mdpreply;
|
||||
bzero(&mdpreply,sizeof(mdpreply));
|
||||
mdpreply.packetTypeAndFlags=MDP_VOMPEVENT;
|
||||
mdpreply.vompevent.flags=VOMPEVENT_CALLINFO;
|
||||
mdpreply.vompevent.call_session_token=mdp->vompevent.call_session_token;
|
||||
if (call) {
|
||||
if (call->audio_started)
|
||||
mdpreply.vompevent.flags|=VOMPEVENT_AUDIOSTREAMING;
|
||||
if (call->remote.state==VOMP_STATE_CALLENDED)
|
||||
mdpreply.vompevent.flags|=VOMPEVENT_CALLENDED;
|
||||
bcopy(call->local.sid,mdpreply.vompevent.local_sid,SID_SIZE);
|
||||
bcopy(call->remote.sid,mdpreply.vompevent.remote_sid,SID_SIZE);
|
||||
bcopy(call->local.did,mdpreply.vompevent.local_did,64);
|
||||
bcopy(call->remote.did,mdpreply.vompevent.remote_did,64);
|
||||
dump_vomp_status();
|
||||
} else
|
||||
if (mdp->vompevent.call_session_token)
|
||||
/* let the requestor know that the requested call doesn't exist */
|
||||
mdpreply.vompevent.flags|=VOMPEVENT_ERROR;
|
||||
|
||||
/* and provide a quick summary of all calls in progress */
|
||||
int i;
|
||||
for(i=0;i<vomp_call_count;i++)
|
||||
{
|
||||
mdpreply.vompevent.other_calls_sessions[i]
|
||||
=vomp_call_states[i].local.session;
|
||||
mdpreply.vompevent.other_calls_states[i]
|
||||
=vomp_call_states[i].local.state;
|
||||
}
|
||||
|
||||
return overlay_mdp_reply(mdp_named.poll.fd,recvaddr,recvaddrlen,&mdpreply);
|
||||
}
|
||||
break;
|
||||
case VOMPEVENT_DIAL:
|
||||
if (vomp_dial(
|
||||
mdp->vompevent.local_sid,
|
||||
mdp->vompevent.remote_sid,
|
||||
NULL,
|
||||
NULL))
|
||||
return overlay_mdp_reply_error
|
||||
(mdp_named.poll.fd,recvaddr,recvaddrlen,4004,
|
||||
"Unable to place call");
|
||||
else{
|
||||
int result= overlay_mdp_reply_error
|
||||
(mdp_named.poll.fd,recvaddr,recvaddrlen,0, "Success");
|
||||
if (result)
|
||||
WHY("Failed to send MDP reply");
|
||||
return result;
|
||||
}
|
||||
break;
|
||||
case VOMPEVENT_CALLREJECT: /* hangup is the same */
|
||||
{
|
||||
struct vomp_call_state *call
|
||||
=vomp_find_call_by_session(mdp->vompevent.call_session_token);
|
||||
if (!call)
|
||||
return overlay_mdp_reply_error(mdp_named.poll.fd,recvaddr, recvaddrlen, 4006, "No such call");
|
||||
vomp_hangup(call);
|
||||
return overlay_mdp_reply_error(mdp_named.poll.fd, recvaddr,recvaddrlen,0,"Success");
|
||||
}
|
||||
break;
|
||||
case VOMPEVENT_PICKUP:
|
||||
{
|
||||
struct vomp_call_state *call = vomp_find_call_by_session(mdp->vompevent.call_session_token);
|
||||
if (!call)
|
||||
return overlay_mdp_reply_error
|
||||
(mdp_named.poll.fd,recvaddr,recvaddrlen,4006,
|
||||
"No such call");
|
||||
|
||||
if (vomp_pickup(call))
|
||||
return overlay_mdp_reply_error(mdp_named.poll.fd,
|
||||
recvaddr,recvaddrlen,4009,
|
||||
"Call is not RINGINGIN, so cannot be picked up");
|
||||
else
|
||||
return overlay_mdp_reply_error(mdp_named.poll.fd,
|
||||
recvaddr,recvaddrlen,0,"Success");
|
||||
}
|
||||
break;
|
||||
case VOMPEVENT_AUDIOPACKET: /* user supplying audio */
|
||||
{
|
||||
// DEBUG("Audio packet arrived");
|
||||
struct vomp_call_state *call = vomp_find_call_by_session(mdp->vompevent.call_session_token);
|
||||
if (call) {
|
||||
return vomp_send_status_remote_audio(call,
|
||||
mdp->vompevent.audio_sample_codec,
|
||||
&mdp->vompevent.audio_bytes[0],
|
||||
vomp_sample_size(mdp->vompevent.audio_sample_codec));
|
||||
}
|
||||
else
|
||||
return WHY("audio packet had invalid call session token");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
/* didn't understand it, so respond with an error */
|
||||
return overlay_mdp_reply_error(mdp_named.poll.fd,
|
||||
recvaddr,recvaddrlen,4001,
|
||||
"Invalid VOMPEVENT request (use DIAL,HANGUP,CALLREJECT,AUDIOSTREAMING,REGISTERINTERST,WITHDRAWINTERST only)");
|
||||
|
||||
}
|
||||
|
||||
return WHY("Not implemented");
|
||||
}
|
||||
|
||||
int vomp_extract_remote_codec_list(struct vomp_call_state *call,overlay_mdp_frame *mdp)
|
||||
{
|
||||
int i;
|
||||
int ofs=14;
|
||||
|
||||
if (debug & DEBUG_VOMP)
|
||||
dump("codec list mdp frame", (unsigned char *)&mdp->in.payload[0],mdp->in.payload_length);
|
||||
for(i=0;mdp->in.payload[14+i]&&(i<256) &&((14+i)<mdp->in.payload_length);i++) {
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUGF("populating remote codec list with %s", vomp_describe_codec(mdp->in.payload[14+i]));
|
||||
call->remote_codec_list[mdp->in.payload[14+i]]=1;
|
||||
|
||||
for (;ofs<mdp->in.payload_length && mdp->in.payload[ofs];ofs++){
|
||||
call->remote_codec_list[mdp->in.payload[ofs]]=1;
|
||||
}
|
||||
if (!call->initiated_call){
|
||||
ofs++;
|
||||
if (ofs<mdp->in.payload_length)
|
||||
ofs+=strlcpy(call->remote.did, (char *)(mdp->in.payload+ofs), sizeof(call->remote.did));
|
||||
if (ofs<mdp->in.payload_length)
|
||||
ofs+=strlcpy(call->local.did, (char *)(mdp->in.payload+ofs), sizeof(call->local.did));
|
||||
}
|
||||
// TODO send codec list to monitor clients
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -1013,8 +752,7 @@ int vomp_mdp_received(overlay_mdp_frame *mdp)
|
||||
// TODO ignore state changes if sequence is stale?
|
||||
// TODO ignore state changes that seem to go backwards?
|
||||
|
||||
if ((!vomp_interested_usock_count)
|
||||
&&(!monitor_socket_count)
|
||||
if ((!monitor_socket_count)
|
||||
&&(!monitor_client_interested(MONITOR_VOMP)))
|
||||
{
|
||||
/* No registered listener, so we cannot answer the call, so just reject
|
||||
@ -1166,40 +904,6 @@ int vomp_mdp_received(overlay_mdp_frame *mdp)
|
||||
return WHY("Malformed VoMP MDP packet?");
|
||||
}
|
||||
|
||||
char *vomp_describe_state(int state)
|
||||
{
|
||||
switch(state) {
|
||||
case VOMP_STATE_CALLENDED: return "CALLENDED";
|
||||
case VOMP_STATE_INCALL: return "INCALL";
|
||||
case VOMP_STATE_RINGINGIN: return "RINGINGIN";
|
||||
case VOMP_STATE_RINGINGOUT: return "RINGINGOUT";
|
||||
case VOMP_STATE_CALLPREP: return "CALLPREP";
|
||||
case VOMP_STATE_NOCALL: return "NOCALL";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
static int dump_vomp_status()
|
||||
{
|
||||
int i;
|
||||
DEBUGF(">>> Active VoMP call states:");
|
||||
for(i=0;i<vomp_call_count;i++)
|
||||
{
|
||||
DEBUGF("%s/%06x -> %s/%06x (%s -> %s)",
|
||||
alloca_tohex_sid(vomp_call_states[i].local.sid),
|
||||
vomp_call_states[i].local.session,
|
||||
alloca_tohex_sid(vomp_call_states[i].remote.sid),
|
||||
vomp_call_states[i].remote.session,
|
||||
vomp_call_states[i].local.did,
|
||||
vomp_call_states[i].remote.did);
|
||||
DEBUGF(" local state=%s, remote state=%s",
|
||||
vomp_describe_state(vomp_call_states[i].local.state),
|
||||
vomp_describe_state(vomp_call_states[i].remote.state));
|
||||
}
|
||||
if (!vomp_call_count) DEBUGF("No active calls");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char *vomp_describe_codec(int c)
|
||||
{
|
||||
switch(c) {
|
||||
@ -1220,19 +924,6 @@ static const char *vomp_describe_codec(int c)
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
strbuf strbuf_append_vomp_supported_codecs(strbuf sb, const unsigned char supported_codecs[256])
|
||||
{
|
||||
int i;
|
||||
const char *p = strbuf_end(sb);
|
||||
for (i = 0; i < 256; ++i)
|
||||
if (supported_codecs[i]) {
|
||||
if (p != strbuf_end(sb))
|
||||
strbuf_putc(sb, ' ');
|
||||
strbuf_puts(sb, vomp_describe_codec(i));
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
int vomp_sample_size(int c)
|
||||
{
|
||||
switch(c) {
|
||||
@ -1274,106 +965,6 @@ int vomp_codec_timespan(int c)
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
int app_vomp_status(int argc, const char *const *argv, struct command_line_option *o)
|
||||
{
|
||||
overlay_mdp_frame mdp;
|
||||
bzero(&mdp,sizeof(mdp));
|
||||
|
||||
mdp.packetTypeAndFlags=MDP_VOMPEVENT;
|
||||
mdp.vompevent.flags=VOMPEVENT_CALLINFO;
|
||||
if (overlay_mdp_send(&mdp,MDP_AWAITREPLY,5000))
|
||||
{
|
||||
WHY("Current call state information request failed.");
|
||||
if (mdp.packetTypeAndFlags==MDP_ERROR&&mdp.error.error)
|
||||
WHYF("MDP: error=%d, message='%s'",
|
||||
mdp.error.error,mdp.error.message);
|
||||
overlay_mdp_client_done();
|
||||
return -1;
|
||||
}
|
||||
if (mdp.packetTypeAndFlags!=MDP_VOMPEVENT) {
|
||||
WHYF("Received incorrect reply type from server (received MDP message type 0x%04x)",mdp.packetTypeAndFlags);
|
||||
overlay_mdp_client_done();
|
||||
return -1;
|
||||
}
|
||||
if (mdp.vompevent.flags!=VOMPEVENT_CALLINFO) {
|
||||
WHYF("Received incorrect reply type from server (received VoMP message type 0x%04x)",mdp.vompevent.flags);
|
||||
overlay_mdp_client_done();
|
||||
return -1;
|
||||
}
|
||||
int i;
|
||||
int count=0;
|
||||
overlay_mdp_frame mdp2;
|
||||
bzero(&mdp2,sizeof(mdp2));
|
||||
for(i=0;i<VOMP_MAX_CALLS;i++)
|
||||
if (mdp.vompevent.other_calls_sessions[i])
|
||||
{
|
||||
count++;
|
||||
if (debug & DEBUG_VOMP) {
|
||||
strbuf b = strbuf_alloca(1024);
|
||||
strbuf_sprintf(b, "%06x:%s:",
|
||||
mdp.vompevent.other_calls_sessions[i],
|
||||
vomp_describe_state(mdp.vompevent.other_calls_states[i]));
|
||||
mdp2.packetTypeAndFlags=MDP_VOMPEVENT;
|
||||
mdp2.vompevent.flags=VOMPEVENT_CALLINFO;
|
||||
mdp2.vompevent.call_session_token=mdp.vompevent.other_calls_sessions[i];
|
||||
if (overlay_mdp_send(&mdp2,MDP_AWAITREPLY,5000))
|
||||
strbuf_puts(b, "<server failed to provide detail>");
|
||||
else {
|
||||
if (mdp2.vompevent.call_session_token!=mdp.vompevent.other_calls_sessions[i])
|
||||
strbuf_sprintf(b, "<strange reply from server (%04x, %04x, token %06x)>",
|
||||
mdp.packetTypeAndFlags,mdp.vompevent.flags,
|
||||
mdp2.vompevent.call_session_token);
|
||||
else {
|
||||
strbuf_sprintf(b, "%s* -> %s* (%s -> %s)",
|
||||
alloca_tohex(mdp2.vompevent.local_sid, 6),
|
||||
alloca_tohex(mdp2.vompevent.remote_sid, 6),
|
||||
strlen(mdp2.vompevent.local_did)
|
||||
?mdp2.vompevent.local_did:"<no local number>",
|
||||
strlen(mdp2.vompevent.remote_did)
|
||||
?mdp2.vompevent.remote_did:"<no remote number>");
|
||||
}
|
||||
strbuf_puts(b, " supports ");
|
||||
strbuf_append_vomp_supported_codecs(b, mdp2.vompevent.supported_codecs);
|
||||
}
|
||||
DEBUG(strbuf_str(b));
|
||||
}
|
||||
}
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUGF("%d live call descriptors.",count);
|
||||
return overlay_mdp_client_done();
|
||||
}
|
||||
|
||||
int app_vomp_dial(int argc, const char *const *argv, struct command_line_option *o)
|
||||
{
|
||||
if (debug & DEBUG_VERBOSE) DEBUG_argv("command", argc, argv);
|
||||
const char *sid,*did,*callerid;
|
||||
cli_arg(argc, argv, o, "sid", &sid, NULL, "");
|
||||
cli_arg(argc, argv, o, "did", &did, NULL, "");
|
||||
cli_arg(argc, argv, o, "callerid", &callerid, NULL, NULL);
|
||||
|
||||
overlay_mdp_frame mdp;
|
||||
bzero(&mdp,sizeof(mdp));
|
||||
|
||||
mdp.packetTypeAndFlags=MDP_VOMPEVENT;
|
||||
mdp.vompevent.flags=VOMPEVENT_DIAL;
|
||||
if (overlay_mdp_getmyaddr(0,&mdp.vompevent.local_sid[0])) return -1;
|
||||
stowSid(&mdp.vompevent.remote_sid[0],0,sid);
|
||||
if (debug & DEBUG_VOMP) {
|
||||
DEBUGF("local_sid=%s",alloca_tohex_sid(mdp.vompevent.local_sid));
|
||||
DEBUGF("remote_sid=%s from %s", alloca_tohex_sid(mdp.vompevent.remote_sid),sid);
|
||||
}
|
||||
if (overlay_mdp_send(&mdp,MDP_AWAITREPLY,5000))
|
||||
WHY("Dial request failed.");
|
||||
if (mdp.packetTypeAndFlags==MDP_ERROR&&mdp.error.error)
|
||||
WHYF("Dial request failed: error=%d, message='%s'", mdp.error.error,mdp.error.message);
|
||||
else {
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUGF("Dial request accepted.");
|
||||
}
|
||||
return overlay_mdp_client_done();
|
||||
}
|
||||
|
||||
int vomp_parse_dtmf_digit(char c)
|
||||
{
|
||||
if (c>='0'&&c<='9') return c-0x30;
|
||||
@ -1398,167 +989,6 @@ char vomp_dtmf_digit_to_char(int digit)
|
||||
return '?';
|
||||
}
|
||||
|
||||
int app_vomp_dtmf(int argc, const char *const *argv, struct command_line_option *o)
|
||||
{
|
||||
const char *call_token;
|
||||
const char *digits;
|
||||
cli_arg(argc, argv, o, "call", &call_token, NULL, "");
|
||||
cli_arg(argc, argv, o, "digits", &digits,NULL,"");
|
||||
|
||||
overlay_mdp_frame mdp;
|
||||
bzero(&mdp,sizeof(mdp));
|
||||
|
||||
mdp.packetTypeAndFlags=MDP_VOMPEVENT;
|
||||
mdp.vompevent.flags=VOMPEVENT_AUDIOPACKET;
|
||||
mdp.vompevent.call_session_token=strtol(call_token,NULL,16);
|
||||
|
||||
/* One digit per sample block. */
|
||||
mdp.vompevent.audio_sample_codec=VOMP_CODEC_DTMF;
|
||||
mdp.vompevent.audio_sample_bytes=1;
|
||||
|
||||
int i;
|
||||
for(i=0;i<strlen(digits);i++) {
|
||||
int digit=vomp_parse_dtmf_digit(digits[i]);
|
||||
if (digit<0)
|
||||
return WHYF("'%c' is not a DTMF digit.",digits[i]);
|
||||
mdp.vompevent.audio_bytes[mdp.vompevent.audio_sample_bytes]
|
||||
=(digit<<4); /* 80ms standard tone duration, so that it is a multiple
|
||||
of the majority of codec time units (70ms is the nominal
|
||||
DTMF tone length for most systems). */
|
||||
if (overlay_mdp_send(&mdp,0,0))
|
||||
WHY("Send DTMF failed");
|
||||
}
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUGF("DTMF digit(s) sent.");
|
||||
return overlay_mdp_client_done();
|
||||
}
|
||||
|
||||
|
||||
int app_vomp_pickup(int argc, const char *const *argv, struct command_line_option *o)
|
||||
{
|
||||
const char *call_token;
|
||||
cli_arg(argc, argv, o, "call", &call_token, NULL, "");
|
||||
|
||||
overlay_mdp_frame mdp;
|
||||
bzero(&mdp,sizeof(mdp));
|
||||
|
||||
mdp.packetTypeAndFlags=MDP_VOMPEVENT;
|
||||
mdp.vompevent.flags=VOMPEVENT_PICKUP;
|
||||
mdp.vompevent.call_session_token=strtol(call_token,NULL,16);
|
||||
|
||||
if (overlay_mdp_send(&mdp,MDP_AWAITREPLY,5000))
|
||||
WHY("Pickup request failed.");
|
||||
if (mdp.packetTypeAndFlags==MDP_ERROR&&mdp.error.error)
|
||||
WHYF("Pickup request failed: error=%d, message='%s'", mdp.error.error,mdp.error.message);
|
||||
else {
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUGF("Pickup request accepted.");
|
||||
}
|
||||
return overlay_mdp_client_done();
|
||||
}
|
||||
|
||||
int app_vomp_hangup(int argc, const char *const *argv, struct command_line_option *o)
|
||||
{
|
||||
const char *call_token;
|
||||
cli_arg(argc, argv, o, "call", &call_token, NULL, "");
|
||||
|
||||
overlay_mdp_frame mdp;
|
||||
bzero(&mdp,sizeof(mdp));
|
||||
|
||||
mdp.packetTypeAndFlags=MDP_VOMPEVENT;
|
||||
mdp.vompevent.flags=VOMPEVENT_HANGUP;
|
||||
mdp.vompevent.call_session_token=strtol(call_token,NULL,16);
|
||||
|
||||
if (overlay_mdp_send(&mdp,MDP_AWAITREPLY,5000))
|
||||
{
|
||||
WHY("Hangup/reject request failed.");
|
||||
}
|
||||
if (mdp.packetTypeAndFlags==MDP_ERROR&&mdp.error.error)
|
||||
WHYF("Hangup/reject request failed: error=%d, message='%s'", mdp.error.error,mdp.error.message);
|
||||
else {
|
||||
if (debug & DEBUG_VOMP)
|
||||
DEBUGF("Hangup/reject request accepted.");
|
||||
}
|
||||
return overlay_mdp_client_done();
|
||||
}
|
||||
|
||||
int app_vomp_monitor(int argc, const char *const *argv, struct command_line_option *o)
|
||||
{
|
||||
overlay_mdp_frame mdp;
|
||||
bzero(&mdp,sizeof(mdp));
|
||||
|
||||
mdp.packetTypeAndFlags=MDP_VOMPEVENT;
|
||||
mdp.vompevent.flags=VOMPEVENT_REGISTERINTEREST;
|
||||
mdp.vompevent.supported_codecs[0]=VOMP_CODEC_DTMF;
|
||||
mdp.vompevent.supported_codecs[1]=VOMP_CODEC_NONE;
|
||||
|
||||
if (overlay_mdp_send(&mdp,MDP_AWAITREPLY,5000)) {
|
||||
WHY("Failed to register interest in telephony events.");
|
||||
overlay_mdp_client_done();
|
||||
if (mdp.packetTypeAndFlags==MDP_ERROR&&mdp.error.error)
|
||||
WHYF(" MDP Server error #%d: '%s'", mdp.error.error,mdp.error.message);
|
||||
return -1;
|
||||
}
|
||||
|
||||
while(!servalShutdown) {
|
||||
overlay_mdp_frame rx;
|
||||
int ttl;
|
||||
/* In theory we should be able to ask for a timeout of -1 for
|
||||
infinity, but broken poll() and select() implementations on OSX
|
||||
make this impossible. So one unnecessary check per second is
|
||||
probably tolerable. */
|
||||
if (overlay_mdp_client_poll(1000) > 0 && !overlay_mdp_recv(&rx,&ttl)) {
|
||||
switch(rx.packetTypeAndFlags) {
|
||||
case MDP_ERROR:
|
||||
WHYF("MDP Server error #%d: '%s'", rx.error.error,rx.error.message);
|
||||
break;
|
||||
case MDP_VOMPEVENT: {
|
||||
strbuf b = strbuf_alloca(1024);
|
||||
strbuf_sprintf(b, "VoMP call descriptor %06x %s:%s",
|
||||
rx.vompevent.call_session_token,
|
||||
vomp_describe_state(rx.vompevent.local_state),
|
||||
vomp_describe_state(rx.vompevent.remote_state));
|
||||
if (rx.vompevent.flags&VOMPEVENT_RINGING)
|
||||
strbuf_puts(b, " RINGING");
|
||||
if (rx.vompevent.flags&VOMPEVENT_CALLENDED)
|
||||
strbuf_puts(b, " CALLENDED");
|
||||
if (rx.vompevent.flags&VOMPEVENT_CALLREJECT)
|
||||
strbuf_puts(b, " CALLREJECTED");
|
||||
if (rx.vompevent.flags&VOMPEVENT_CALLCREATED)
|
||||
strbuf_puts(b, " CREATED");
|
||||
if (rx.vompevent.flags&VOMPEVENT_AUDIOSTREAMING)
|
||||
strbuf_puts(b, " AUDIOSTREAMING");
|
||||
strbuf_puts(b, " codecs: ");
|
||||
strbuf_append_vomp_supported_codecs(b, rx.vompevent.supported_codecs);
|
||||
DEBUG(strbuf_str(b));
|
||||
if (rx.vompevent.audio_sample_codec) {
|
||||
DEBUGF(" attached audio sample: codec=%s, len=%d",
|
||||
vomp_describe_codec(rx.vompevent.audio_sample_codec),
|
||||
rx.vompevent.audio_sample_bytes);
|
||||
DEBUGF(" sample covers %ldms - %ldms of call.",
|
||||
(long) rx.vompevent.audio_sample_starttime,
|
||||
(long) rx.vompevent.audio_sample_endtime);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
DEBUGF("Unknown message (type=0x%02x)",rx.packetTypeAndFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mdp.packetTypeAndFlags=MDP_VOMPEVENT;
|
||||
mdp.vompevent.flags=VOMPEVENT_WITHDRAWINTEREST;
|
||||
if (overlay_mdp_send(&mdp,MDP_AWAITREPLY,5000)) {
|
||||
WHY("Failed to deregister interest in telephony events.");
|
||||
overlay_mdp_client_done();
|
||||
return -1;
|
||||
}
|
||||
if (mdp.packetTypeAndFlags==MDP_ERROR&&mdp.error.error)
|
||||
DEBUGF("MDP Server error #%d: '%s'", mdp.error.error,mdp.error.message);
|
||||
return overlay_mdp_client_done();
|
||||
}
|
||||
|
||||
static void vomp_process_tick(struct sched_ent *alarm)
|
||||
{
|
||||
char msg[32];
|
||||
|
Loading…
x
Reference in New Issue
Block a user