实例介绍
【实例简介】
【实例截图】
【核心代码】
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
using System.Net;
using System.Threading;
using LumiSoft.Net;
using LumiSoft.Net.SDP;
using LumiSoft.Net.SIP;
using LumiSoft.Net.SIP.Debug;
using LumiSoft.Net.SIP.Stack;
using LumiSoft.Net.SIP.Message;
using LumiSoft.Net.Media;
using LumiSoft.Net.Media.Codec;
using LumiSoft.Net.Media.Codec.Audio;
using LumiSoft.Net.RTP;
using LumiSoft.Net.RTP.Debug;
using LumiSoft.Net.STUN.Client;
using LumiSoft.Net.UPnP.NAT;
using LumiSoft.SIP.UA;
using LumiSoft.SIP.UA.Resources;
using LumiSoft.Net.AUTH;
namespace LumiSoft.SIP.UA.UI
{
/// <summary>
/// Application main window.
/// </summary>
public class wfrm_Main : Form
{
private MenuStrip m_pMenu = null;
private ToolStrip m_pToolbar = null;
private ComboBox m_pAccounts = null;
private Button m_pCall = null;
private Button m_pIM = null;
private ComboBox m_pCallUriType = null;
private ComboBox m_pCallUri = null;
private TabControl m_pCallTab = null;
private ListView m_pCallTab_Contacts_Contacts = null;
private wctrl_CallList m_pCallTab_Calls_Calls = null;
private ListView m_pCallTab_Status_Registrations = null;
private bool m_IsDisposing = false;
private bool m_IsDebug = true;
private Settings m_pSettings = null;
private SIP_Stack m_pStack = null;
private List<wfrm_IM> m_pActiveIM = null;
private wfrm_SIP_Debug m_pDebugFrom = null;
private string m_NatHandlingType = "";
private string m_StunServer = "172.17.17.131";
private UPnP_NAT_Client m_pUPnP = null;
private Dictionary<int,AudioCodec> m_pAudioCodecs = null;
private AudioOutDevice m_pAudioOutDevice = null;
private AudioInDevice m_pAudioInDevice = null;
private int m_RtpBasePort = 12000;
/// <summary>
/// Default constructor.
/// </summary>
/// <param name="isDebug">Specifies if debug windows showed.</param>
public wfrm_Main(bool isDebug)
{
InitUI();
m_IsDebug = isDebug;
m_pSettings = new Settings();
m_pStack = new SIP_Stack();
m_pStack.UserAgent = "LumiSoft SIP UA 1.0";
m_pStack.Error = new EventHandler<ExceptionEventArgs>(m_pStack_Error);
m_pActiveIM = new List<wfrm_IM>();
// Show SIP debug UI if in debug mode.
if(m_IsDebug){
m_pDebugFrom = new wfrm_SIP_Debug(m_pStack);
m_pDebugFrom.Show();
}
// Remove me:
m_pCallUri.Items.Add("1003@172.17.17.131");
m_pCallUri.Items.Add("tel@lumisoft.ee");
m_pCallUri.Items.Add("music@iptel.org");
m_pCallUri.Items.Add("4151595@services.sip.is");
}
#region method InitUI
/// <summary>
/// Creates and initializes UI.
/// </summary>
private void InitUI()
{
this.ClientSize = new Size(300,400);
this.StartPosition = FormStartPosition.CenterScreen;
this.Icon = ResManager.GetIcon("app.ico",new Size(16,16));
this.Text = "SIP";
this.FormClosed = new FormClosedEventHandler(wfrm_Main_FormClosed);
this.VisibleChanged = new EventHandler(wfrm_Main_VisibleChanged);
m_pMenu = new MenuStrip();
m_pMenu.Size = new Size(140,25);
m_pMenu.Location = new Point(0,0);
m_pMenu.Dock = DockStyle.None;
ToolStripMenuItem menu_file = new ToolStripMenuItem("File");
ToolStripMenuItem menu_file_exit = new ToolStripMenuItem("Exit");
menu_file_exit.Tag = "exit";
menu_file_exit.Click = new EventHandler(m_pMenu_ItemClicked);
menu_file.DropDownItems.Add(menu_file_exit);
m_pMenu.Items.Add(menu_file);
ToolStripMenuItem menu_settings = new ToolStripMenuItem("Settings");
ToolStripMenuItem menu_settings_proxy = new ToolStripMenuItem("Proxy Settings");
menu_settings_proxy.Tag = "proxy";
menu_settings_proxy.Click = new EventHandler(m_pMenu_ItemClicked);
menu_settings.DropDownItems.Add(menu_settings_proxy);
ToolStripMenuItem menu_settings_credentials = new ToolStripMenuItem("Credentials");
menu_settings_credentials.Tag = "credentials";
menu_settings_credentials.Click = new EventHandler(m_pMenu_ItemClicked);
menu_settings.DropDownItems.Add(menu_settings_credentials);
ToolStripMenuItem menu_settings_accounts = new ToolStripMenuItem("Accounts");
menu_settings_accounts.Tag = "accounts";
menu_settings_accounts.Click = new EventHandler(m_pMenu_ItemClicked);
menu_settings.DropDownItems.Add(menu_settings_accounts);
ToolStripMenuItem menu_settings_audio = new ToolStripMenuItem("Audio");
menu_settings_audio.Tag = "audio";
menu_settings_audio.Click = new EventHandler(m_pMenu_ItemClicked);
menu_settings.DropDownItems.Add(menu_settings_audio);
m_pMenu.Items.Add(menu_settings);
m_pToolbar = new ToolStrip();
m_pToolbar.Size = new Size(70,20);
m_pToolbar.Location = new Point(195,0);
m_pToolbar.Dock = DockStyle.None;
m_pToolbar.Renderer = new ToolBarRendererEx();
//--- Audio button
ToolStripDropDownButton button_Audio = new ToolStripDropDownButton();
button_Audio.Name = "audio";
button_Audio.Image = ResManager.GetIcon("speaker.ico").ToBitmap();
foreach(AudioOutDevice device in AudioOut.Devices){
ToolStripMenuItem item = new ToolStripMenuItem(device.Name);
item.Checked = (button_Audio.DropDownItems.Count == 0);
item.Tag = device;
button_Audio.DropDownItems.Add(item);
}
button_Audio.DropDown.ItemClicked = new ToolStripItemClickedEventHandler(m_pToolBar_Audio_ItemClicked);
m_pToolbar.Items.Add(button_Audio);
//--- Microphone button
ToolStripDropDownButton button_Mic = new ToolStripDropDownButton();
button_Mic.Name = "mic";
button_Mic.Image = ResManager.GetIcon("mic.ico").ToBitmap();
foreach(AudioInDevice device in AudioIn.Devices){
ToolStripMenuItem item = new ToolStripMenuItem(device.Name);
item.Checked = (button_Mic.DropDownItems.Count == 0);
item.Tag = device;
button_Mic.DropDownItems.Add(item);
}
button_Mic.DropDown.ItemClicked = new ToolStripItemClickedEventHandler(m_pToolBar_Mic_ItemClicked);
m_pToolbar.Items.Add(button_Mic);
// Separator
m_pToolbar.Items.Add(new ToolStripSeparator());
// NAT
ToolStripDropDownButton button_NAT = new ToolStripDropDownButton();
button_NAT.Name = "nat";
button_NAT.Image = ResManager.GetIcon("router.ico").ToBitmap();
button_NAT.DropDown.ItemClicked = new ToolStripItemClickedEventHandler(m_pToolbar_NAT_DropDown_ItemClicked);
m_pToolbar.Items.Add(button_NAT);
m_pAccounts = new ComboBox();
m_pAccounts.Size = new Size(190,20);
m_pAccounts.Location = new Point(5,40);
m_pAccounts.DropDownStyle = ComboBoxStyle.DropDownList;
m_pCall = new Button();
m_pCall.Size = new Size(45,45);
m_pCall.Location = new Point(202,40);
m_pCall.Image = ResManager.GetIcon("call.ico",new Size(24,24)).ToBitmap();
m_pCall.Click = new EventHandler(m_pCall_Click);
m_pIM = new Button();
m_pIM.Size = new Size(45,45);
m_pIM.Location = new Point(250,40);
m_pIM.Image = ResManager.GetIcon("im.ico",new Size(24,24)).ToBitmap();
m_pIM.Click = new EventHandler(m_pIM_Click);
m_pCallUriType = new ComboBox();
m_pCallUriType.Size = new Size(45,20);
m_pCallUriType.Location = new Point(5,65);
m_pCallUriType.Items.Add("sip:");
m_pCallUriType.SelectedIndex = 0;
m_pCallUri = new ComboBox();
m_pCallUri.Size = new Size(145,20);
m_pCallUri.Location = new Point(50,65);
m_pCallTab = new TabControl();
m_pCallTab.Size = new Size(300,300);
m_pCallTab.Location = new Point(0,100);
m_pCallTab.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top;
m_pCallTab.TabPages.Add("contacts","Contacts");
m_pCallTab.TabPages.Add("calls","Active Calls");
m_pCallTab.TabPages.Add("history","History");
m_pCallTab.TabPages.Add("status","Status");
#region Contacts tab
m_pCallTab.TabPages["contacts"].BackColor = Color.White;
m_pCallTab_Contacts_Contacts = new ListView();
m_pCallTab_Contacts_Contacts.Size = new Size(260,200);
m_pCallTab_Contacts_Contacts.Location = new Point(0,0);
m_pCallTab_Contacts_Contacts.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top;
m_pCallTab_Contacts_Contacts.BorderStyle = BorderStyle.None;
m_pCallTab_Contacts_Contacts.View = View.Details;
m_pCallTab_Contacts_Contacts.FullRowSelect = true;
m_pCallTab_Contacts_Contacts.HeaderStyle = ColumnHeaderStyle.None;
m_pCallTab_Contacts_Contacts.MouseUp = new MouseEventHandler(m_pCallTab_Contacts_Contacts_MouseUp);
m_pCallTab_Contacts_Contacts.Columns.Add("Name",240);
m_pCallTab.TabPages["contacts"].Controls.Add(m_pCallTab_Contacts_Contacts);
#endregion
#region Acive calls tab
m_pCallTab.TabPages["calls"].BackColor = Color.White;
m_pCallTab_Calls_Calls = new wctrl_CallList();
m_pCallTab_Calls_Calls.Dock = DockStyle.Fill;
m_pCallTab_Calls_Calls.BorderStyle = BorderStyle.None;
m_pCallTab.TabPages["calls"].Controls.Add(m_pCallTab_Calls_Calls);
#endregion
#region Status tab
m_pCallTab.TabPages["status"].BackColor = Color.White;
ImageList callTab_Satus_RegistrationsImages = new ImageList();
callTab_Satus_RegistrationsImages.Images.Add(ResManager.GetIcon("register_ok.ico",new Size(16,16)));
callTab_Satus_RegistrationsImages.Images.Add(ResManager.GetIcon("register_error.ico",new Size(16,16)));
callTab_Satus_RegistrationsImages.Images.Add(ResManager.GetIcon("register_registering.ico",new Size(16,16)));
m_pCallTab_Status_Registrations = new ListView();
m_pCallTab_Status_Registrations.Size = new Size(260,200);
m_pCallTab_Status_Registrations.Location = new Point(0,0);
m_pCallTab_Status_Registrations.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top;
m_pCallTab_Status_Registrations.BorderStyle = BorderStyle.None;
m_pCallTab_Status_Registrations.View = View.Details;
m_pCallTab_Status_Registrations.FullRowSelect = true;
m_pCallTab_Status_Registrations.ShowItemToolTips = true;
m_pCallTab_Status_Registrations.SmallImageList = callTab_Satus_RegistrationsImages;
m_pCallTab_Status_Registrations.Columns.Add("Registraion",180);
m_pCallTab_Status_Registrations.Columns.Add("Status",80);
m_pCallTab.TabPages["status"].Controls.Add(m_pCallTab_Status_Registrations);
#endregion
this.Controls.Add(m_pMenu);
this.Controls.Add(m_pToolbar);
this.Controls.Add(m_pAccounts);
this.Controls.Add(m_pIM);
this.Controls.Add(m_pCallUriType);
this.Controls.Add(m_pCallUri);
this.Controls.Add(m_pCall);
this.Controls.Add(m_pCallTab);
m_pCallTab.SelectedTab = m_pCallTab.TabPages["status"];
}
#endregion
#region Events handling
#region method m_pMenu_ItemClicked
/// <summary>
/// This method is called when main menu item clicked.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void m_pMenu_ItemClicked(object sender,EventArgs e)
{
ToolStripMenuItem item = (ToolStripMenuItem)sender;
if(item.Tag == null){
return;
}
else if(item.Tag.ToString() == "accounts"){
wfrm_Settings frm = new wfrm_Settings();
frm.ShowDialog(this);
/*
wfrm_Settings_Accounts frm = new wfrm_Settings_Accounts();
if(frm.ShowDialog(this) == DialogResult.OK){
Restart();
}*/
}
else if(item.Tag.ToString() == "exit"){
this.Close();
}
}
#endregion
#region method m_pToolBar_Audio_ItemClicked
/// <summary>
/// Is called when new audio-out device is selected.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void m_pToolBar_Audio_ItemClicked(object sender,ToolStripItemClickedEventArgs e)
{
try{
foreach(ToolStripMenuItem item in ((ToolStripDropDownMenu)sender).Items){
if(item.Equals(e.ClickedItem)){
item.Checked = true;
}
else{
item.Checked = false;
}
}
m_pAudioOutDevice = (AudioOutDevice)e.ClickedItem.Tag;
foreach(wctrl_Call call in m_pCallTab_Calls_Calls.Calls){
call.AudioOutDevice = m_pAudioOutDevice;
}
}
catch(Exception x){
MessageBox.Show("Error: " x.Message,"Error:",MessageBoxButtons.OK,MessageBoxIcon.Error);
}
}
#endregion
#region method m_pToolBar_Mic_ItemClicked
/// <summary>
/// Is called when new audio-in device is selected.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void m_pToolBar_Mic_ItemClicked(object sender,ToolStripItemClickedEventArgs e)
{
try{
foreach(ToolStripMenuItem item in ((ToolStripDropDownMenu)sender).Items){
if(item.Equals(e.ClickedItem)){
item.Checked = true;
}
else{
item.Checked = false;
}
}
m_pAudioInDevice = (AudioInDevice)e.ClickedItem.Tag;
foreach(wctrl_Call call in m_pCallTab_Calls_Calls.Calls){
call.AudioInDevice = m_pAudioInDevice;
}
}
catch(Exception x){
MessageBox.Show("Error: " x.Message,"Error:",MessageBoxButtons.OK,MessageBoxIcon.Error);
}
}
#endregion
#region method m_pToolbar_NAT_DropDown_ItemClicked
/// <summary>
/// Is called when new NAT handling method is selected.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void m_pToolbar_NAT_DropDown_ItemClicked(object sender,ToolStripItemClickedEventArgs e)
{
if(!e.ClickedItem.Enabled){
return;
}
foreach(ToolStripMenuItem item in ((ToolStripDropDownMenu)sender).Items){
if(item.Equals(e.ClickedItem)){
item.Checked = true;
m_NatHandlingType = item.Name;
}
else{
item.Checked = false;
}
}
}
#endregion
#region method m_pCall_Click
/// <summary>
/// This method is called when call button is pressed.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void m_pCall_Click(object sender,EventArgs e)
{
if(m_pCallUri.Text == string.Empty){
MessageBox.Show(this,"Please fill call URI !","Error:",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
}
string uri = m_pCallUri.Text;
if(uri.IndexOf(':') == -1){
uri = "sip:" uri;
}
Call(((Account)((WComboBoxItem)m_pAccounts.SelectedItem).Tag),new SIP_t_NameAddress(uri));
m_pCallUri.Text = "";
m_pCallTab.SelectedTab = m_pCallTab.TabPages["calls"];
}
#endregion
#region method m_pIM_Click
/// <summary>
/// This method is called when IM button is clicked.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void m_pIM_Click(object sender,EventArgs e)
{
if(m_pCallUri.Text == string.Empty){
MessageBox.Show(this,"Please fill call URI !","Error:",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
}
string uri = m_pCallUri.Text;
if(uri.IndexOf(':') == -1){
uri = "sip:" uri;
}
OpenIM(((Account)((WComboBoxItem)m_pAccounts.SelectedItem).Tag),new SIP_t_NameAddress(uri));
m_pCallUri.Text = "";
}
#endregion
#region method wfrm_Main_FormClosed
/// <summary>
/// This method is called when main form is closed.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void wfrm_Main_FormClosed(object sender,FormClosedEventArgs e)
{
m_IsDisposing = true;
if(m_pDebugFrom != null){
m_pDebugFrom.Close();
}
m_pStack.Stop();
m_pStack.Dispose();
}
#endregion
#region method wfrm_Main_VisibleChanged
/// <summary>
/// This method is called when main windows visibility changes.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void wfrm_Main_VisibleChanged(object sender, EventArgs e)
{
if(this.Visible){
LoadSettings();
}
}
#endregion
#region method IM_FormClosed
/// <summary>
/// This method is called when IM window has closed.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void IM_FormClosed(object sender,FormClosedEventArgs e)
{
m_pActiveIM.Remove((wfrm_IM)sender);
}
#endregion
#region method m_pCallTab_Contacts_Contacts_MouseUp
/// <summary>
/// This method is called when contacts list mouse button is released.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void m_pCallTab_Contacts_Contacts_MouseUp(object sender,MouseEventArgs e)
{
if(e.Button != MouseButtons.Right){
return;
}
ContextMenuStrip menu = new ContextMenuStrip();
ToolStripMenuItem menu_Add = new ToolStripMenuItem("New Contact");
menu_Add.Click = new EventHandler(delegate(object s1,EventArgs e1){
// TODO:
});
menu.Items.Add(menu_Add);
if(m_pCallTab_Contacts_Contacts.SelectedItems.Count > 0){
menu.Items.Add(new ToolStripSeparator());
ToolStripMenuItem menu_Call = new ToolStripMenuItem("Call");
menu_Call.Click = new EventHandler(delegate(object s1,EventArgs e1){
// TODO:
// FIX ME:
string uri = m_pCallTab_Contacts_Contacts.SelectedItems[0].Text;
if(uri.IndexOf(':') == -1){
uri = "sip:" uri;
}
m_pCallTab.SelectedTab = m_pCallTab.TabPages["calls"];
Call(((Account)((WComboBoxItem)m_pAccounts.SelectedItem).Tag),new SIP_t_NameAddress(uri));
});
menu.Items.Add(menu_Call);
ToolStripMenuItem menu_IM = new ToolStripMenuItem("IM");
menu_IM.Click = new EventHandler(delegate(object s1,EventArgs e1){
// TODO:
// FIX ME:
string uri = m_pCallTab_Contacts_Contacts.SelectedItems[0].Text;
if(uri.IndexOf(':') == -1){
uri = "sip:" uri;
}
OpenIM(((Account)((WComboBoxItem)m_pAccounts.SelectedItem).Tag),new SIP_t_NameAddress(uri));
});
menu.Items.Add(menu_IM);
}
menu.Show(m_pCallTab_Contacts_Contacts,e.Location);
}
#endregion
#region method m_pCallTab_Calls_Calls_MouseUp
/// <summary>
/// This method is called when calls list mouse button is released.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void m_pCallTab_Calls_Calls_MouseUp(object sender,MouseEventArgs e)
{
if(e.Button != MouseButtons.Right){
return;
}
/*
if(m_pCallTab_Calls_Calls.SelectedItems.Count > 0){
Call call = (Call)m_pCallTab_Calls_Calls.SelectedItems[0].Tag;
ContextMenuStrip menu = new ContextMenuStrip();
if(call.State == CallState.WaitingToAccept){
// Accept
ToolStripMenuItem accept = new ToolStripMenuItem("Accept");
accept.Click = new EventHandler(delegate(object s1,EventArgs e1){
AcceptCall(call);
});
menu.Items.Add(accept);
}
if(call.State == CallState.Active){
bool onHold = false;
if(call.Tags.ContainsKey("OnHold")){
onHold = ((bool)call.Tags["OnHold"]);
}
if(onHold){
// Take off on hold
ToolStripMenuItem putOnHold = new ToolStripMenuItem("Take Off On Hold");
putOnHold.Image = ResManager.GetIcon("call_hangup.ico",new Size(16,16)).ToBitmap();
putOnHold.Click = new EventHandler(delegate(object s1,EventArgs e1){
ToggleCallOnHold(call);
});
menu.Items.Add(putOnHold);
}
else{
// Put on hold
ToolStripMenuItem putOnHold = new ToolStripMenuItem("Put On Hold");
putOnHold.Image = ResManager.GetIcon("call_hangup.ico",new Size(16,16)).ToBitmap();
putOnHold.Click = new EventHandler(delegate(object s1,EventArgs e1){
ToggleCallOnHold(call);
});
menu.Items.Add(putOnHold);
}
}
// Separator
if(menu.Items.Count > 0){
menu.Items.Add(new ToolStripSeparator());
}
// Hang Up
ToolStripMenuItem hangUp = new ToolStripMenuItem("Hang Up");
hangUp.Image = ResManager.GetIcon("call_hangup.ico",new Size(16,16)).ToBitmap();
hangUp.Click = new EventHandler(delegate(object s1,EventArgs e1){
call.Terminate("Hang Up");
});
menu.Items.Add(hangUp);
//--------
menu.Show(m_pCallTab_Calls_Calls,e.Location);
}*/
}
#endregion
#region method Registration_StateChanged
/// <summary>
/// This method is called when registration state has changed.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void Registration_StateChanged(object sender,EventArgs e)
{
if(m_IsDisposing){
return;
}
try{
this.BeginInvoke(
new MethodInvoker(delegate(){
SIP_UA_Registration registration = (SIP_UA_Registration)sender;
// Remove registration.
if(registration.State == SIP_UA_RegistrationState.Disposed){
foreach(ListViewItem it in m_pCallTab_Status_Registrations.Items){
if(it.Text == registration.AOR){
it.Remove();
break;
}
}
}
// Update registration state.
else{
ListViewItem regItem = null;
foreach(ListViewItem it in m_pCallTab_Status_Registrations.Items){
if(it.Text == registration.AOR){
regItem = it;
break;
}
}
// New registration, create it to UI.
if(regItem == null){
regItem = new ListViewItem(registration.AOR);
regItem.SubItems.Add("");
m_pCallTab_Status_Registrations.Items.Add(regItem);
}
if(registration.State == SIP_UA_RegistrationState.Registered){
regItem.ImageIndex = 0;
StringBuilder tooltipText = new StringBuilder();
tooltipText.AppendLine("AOR: " registration.AOR);
tooltipText.AppendLine("TTL: " registration.Expires);
foreach(AbsoluteUri contactUri in registration.Contacts){
tooltipText.AppendLine("Contact: " contactUri.ToString());
}
regItem.ToolTipText = tooltipText.ToString();
}
else if(registration.State == SIP_UA_RegistrationState.Error){
regItem.ImageIndex = 1;
//regItem.ToolTipText
}
else if(registration.State == SIP_UA_RegistrationState.Registering){
regItem.ImageIndex = 2;
}
else{
regItem.ImageIndex = -1;
}
regItem.SubItems[1].Text = registration.State.ToString();
}
}),
null
);
}
catch(Exception x){
string dummy = x.Message;
// Skip, UA closing.
}
}
#endregion
#region method m_pStack_RequestReceived
/// <summary>
/// This method is called when SIP stack get new incoming request.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void m_pStack_RequestReceived(object sender,SIP_RequestReceivedEventArgs e)
{
try{
#region Validate request-URI
/* Block rules.
*) Block non SIP URI.
*) Block non local AOR.
*/
if(!(e.Request.RequestLine.Uri is SIP_Uri)){
e.ServerTransaction.SendResponse(m_pStack.CreateResponse(SIP_ResponseCodes.x416_Unsupported_URI_Scheme,e.Request));
return;
}
SIP_Uri requestUri = (SIP_Uri)e.Request.RequestLine.Uri;
// If request URI is our registration contact, get AOR for it.
string aor = requestUri.Address;
foreach(SIP_UA_Registration registration in m_pStack.Registrations){
foreach(AbsoluteUri contactUri in registration.Contacts){
if(requestUri.Equals(contactUri)){
aor = registration.AOR;
break;
}
}
}
Account localAccount = null;
foreach(WComboBoxItem it in m_pAccounts.Items){
Account account = (Account)it.Tag;
if(string.Equals(account.AOR,aor,StringComparison.InvariantCultureIgnoreCase)){
localAccount = account;
break;
}
}
if(localAccount == null){
e.ServerTransaction.SendResponse(m_pStack.CreateResponse(SIP_ResponseCodes.x404_Not_Found,e.Request));
return;
}
#endregion
#region MESSAGE
if(e.Request.RequestLine.Method == SIP_Methods.MESSAGE){
wfrm_IM frmIM = null;
// If active IM window, use it.
foreach(wfrm_IM frm in m_pActiveIM){
if(frm.TargetURI.Uri.ToString() == e.Request.From.Address.Uri.ToString()){
frmIM = frm;
break;
}
}
if(frmIM == null){
// See if in contacts list
// Open IM window
// Ask user to allow/block contact
this.Invoke(
new MethodInvoker(delegate(){
frmIM = OpenIM(localAccount,e.Request.From.Address);
}),
null
);
}
frmIM.ProcessIM(e.Request);
// Send 200 response.
e.ServerTransaction.SendResponse(m_pStack.CreateResponse(SIP_ResponseCodes.x200_Ok,e.Request));
}
#endregion
#region INVITE
else if(e.Request.RequestLine.Method == SIP_Methods.INVITE){
#region Incoming call
if(e.Dialog == null){
#region Validate incoming call
// TODO: Limit incoming calls count
// We don't accept more than 1 call at time.
//if(m_pIncomingCallUI != null || m_pCall != null){
// e.ServerTransaction.SendResponse(m_pStack.CreateResponse(SIP_ResponseCodes.x600_Busy_Everywhere,e.Request));
// return;
//}
// We don't accept SDP offerless calls.
if(e.Request.ContentType == null || e.Request.ContentType.ToLower().IndexOf("application/sdp") == -1){
e.ServerTransaction.SendResponse(m_pStack.CreateResponse(SIP_ResponseCodes.x606_Not_Acceptable ": We don't accpet SDP offerless calls.",e.Request));
return;
}
SDP_Message sdpOffer = SDP_Message.Parse(Encoding.UTF8.GetString(e.Request.Data));
// Check if we can accept any media stream.
bool canAccept = false;
foreach(SDP_MediaDescription media in sdpOffer.MediaDescriptions){
if(CanSupportMedia(media)){
canAccept = true;
break;
}
}
if(!canAccept){
e.ServerTransaction.SendResponse(m_pStack.CreateResponse(SIP_ResponseCodes.x606_Not_Acceptable,e.Request));
return;
}
#endregion
// We need BeginInvoke here to access UI, we are running on thread pool thread.
this.BeginInvoke(new MethodInvoker(delegate(){
m_pCallTab.SelectedTab = m_pCallTab.TabPages["calls"];
// Create call, all continuing processing done in call class.
wctrl_Call call = new wctrl_Call(this,m_pAudioOutDevice,m_pAudioInDevice);
m_pCallTab_Calls_Calls.AddCall(call);
call.InitIncoming(localAccount,e.ServerTransaction);
}));
}
#endregion
else{
// Unknown error, call didn't process re-INVITE.
e.ServerTransaction.SendResponse(m_pStack.CreateResponse(SIP_ResponseCodes.x500_Server_Internal_Error,e.Request));
}
}
#endregion
#region CANCEL
else if(e.Request.RequestLine.Method == SIP_Methods.CANCEL){
/* RFC 3261 9.2.
If the UAS did not find a matching transaction for the CANCEL
according to the procedure above, it SHOULD respond to the CANCEL
with a 481 (Call Leg/Transaction Does Not Exist).
Regardless of the method of the original request, as long as the
CANCEL matched an existing transaction, the UAS answers the CANCEL
request itself with a 200 (OK) response.
*/
SIP_ServerTransaction trToCancel = m_pStack.TransactionLayer.MatchCancelToTransaction(e.Request);
if(trToCancel != null){
trToCancel.Cancel();
e.ServerTransaction.SendResponse(m_pStack.CreateResponse(SIP_ResponseCodes.x200_Ok,e.Request));
}
else{
e.ServerTransaction.SendResponse(m_pStack.CreateResponse(SIP_ResponseCodes.x481_Call_Transaction_Does_Not_Exist,e.Request));
}
}
#endregion
#region BYE
else if(e.Request.RequestLine.Method == SIP_Methods.BYE){
/* RFC 3261 15.1.2.
If the BYE does not match an existing dialog, the UAS core SHOULD generate a 481
(Call/Transaction Does Not Exist) response and pass that to the server transaction.
*/
// Currently we match BYE to dialog and it processes it,
// so BYE what reaches here doesnt match to any dialog.
e.ServerTransaction.SendResponse(m_pStack.CreateResponse(SIP_ResponseCodes.x481_Call_Transaction_Does_Not_Exist,e.Request));
}
#endregion
#region ACK
else if(e.Request.RequestLine.Method == SIP_Methods.ACK){
// Abandoned ACK, just skip it.
}
#endregion
#region Other
else
{
// ACK is response less method.
if(e.Request.RequestLine.Method != SIP_Methods.ACK){
e.ServerTransaction.SendResponse(m_pStack.CreateResponse(SIP_ResponseCodes.x501_Not_Implemented,e.Request));
}
}
#endregion
}
catch(Exception x){
e.ServerTransaction.SendResponse(m_pStack.CreateResponse(SIP_ResponseCodes.x500_Server_Internal_Error,e.Request));
OnError(x);
}
}
#endregion
#region method m_pStack_Error
/// <summary>
/// Is called when SIP stack has unhandled error.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event data.</param>
private void m_pStack_Error(object sender,ExceptionEventArgs e)
{
// TODO:
Console.WriteLine("Error: " e.Exception.ToString());
}
#endregion
#endregion
#region method Call
/// <summary>
/// Calls to the specified person.
/// </summary>
/// <param name="account">Local account to use.</param>
/// <param name="to">Call target address.</param>
/// <exception cref="ArgumentNullException">Is raised when <b>account</b> or <b>to</b> is null reference.</exception>
public void Call(Account account, SIP_t_NameAddress to)
{
if (account == null)
{
throw new ArgumentNullException("account");
}
if (to == null)
{
throw new ArgumentNullException("to");
}
PutAllCallsOnHold();
// Create call and start calling.
wctrl_Call call = new wctrl_Call(this, m_pAudioOutDevice, m_pAudioInDevice);
m_pCallTab_Calls_Calls.AddCall(call);
// Move processing to thread pool, otherwise UI hangs.
ThreadPool.QueueUserWorkItem(delegate(object state)
{
call.InitCalling(account, to);
});
}
#endregion
#region method OpenIM
/// <summary>
/// Opens new IM window to the specified recipient.
/// </summary>
/// <param name="account">Local account.</param>
/// <param name="to">Remote party.</param>
/// <returns>Returns created IM window.</returns>
/// <exception cref="ArgumentNullException">Is raised when <b>account</b> or <b>to</b> is null reference.</exception>
public wfrm_IM OpenIM(Account account,SIP_t_NameAddress to)
{
if(account == null){
throw new ArgumentNullException("account");
}
if(to == null){
throw new ArgumentNullException("to");
}
wfrm_IM frmIM = new wfrm_IM(this,account,to);
m_pActiveIM.Add(frmIM);
frmIM.FormClosed = new FormClosedEventHandler(IM_FormClosed);
frmIM.Visible = true;
return frmIM;
}
#endregion
#region method PutAllCallsOnHold
/// <summary>
/// Puts all calls on hold.
/// </summary>
internal void PutAllCallsOnHold()
{
// Put all active calls on hold.
foreach(wctrl_Call c in this.Calls){
if(c.State == SIP_CallState.Active && !c.IsLocalOnHold){
c.PutCallOnHold();
}
}
// Wait calls to switch to on-hold.
DateTime start = DateTime.Now;
while(start.AddSeconds(5) > DateTime.Now){
bool allonHold = true;
foreach(wctrl_Call c in this.Calls){
if(c.State == SIP_CallState.Active && !c.IsLocalOnHold){
allonHold = false;
}
}
if(allonHold){
break;
}
else{
Thread.Sleep(200);
}
}
}
#endregion
#region method LoadSettings
/// <summary>
/// Loads settings from xml file.
/// </summary>
private void LoadSettings()
{
if(File.Exists(Application.StartupPath "\\Settings\\Settings.xml")){
this.Settings.Load(Application.StartupPath "\\Settings\\Settings.xml");
}
#region Init audio devices
if(AudioOut.Devices.Length == 0){
foreach(Control control in this.Controls){
control.Enabled = false;
}
MessageBox.Show("Calling not possible, there are no speakers in computer.","Error:",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
}
if(AudioIn.Devices.Length == 0){
foreach(Control control in this.Controls){
control.Enabled = false;
}
MessageBox.Show("Calling not possible, there is no microphone in computer.","Error:",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
}
m_pAudioOutDevice = AudioOut.Devices[0];
m_pAudioInDevice = AudioIn.Devices[0];
m_pAudioCodecs = new Dictionary<int,AudioCodec>();
m_pAudioCodecs.Add(0,new PCMU());
m_pAudioCodecs.Add(8,new PCMA());
#endregion
#region Get NAT handling methods
m_pUPnP = new UPnP_NAT_Client();
STUN_Result stunResult = new STUN_Result(STUN_NetType.UdpBlocked,null);
try{
stunResult = STUN_Client.Query(m_StunServer, 5060, new IPEndPoint(IPAddress.Any, 0));
}
catch{
}
if(stunResult.NetType == STUN_NetType.Symmetric || stunResult.NetType == STUN_NetType.UdpBlocked){
ToolStripMenuItem item_stun = new ToolStripMenuItem("STUN (" stunResult.NetType ")");
item_stun.Name = "stun";
item_stun.Enabled = false;
((ToolStripDropDownButton)m_pToolbar.Items["nat"]).DropDownItems.Add(item_stun);
}
else{
ToolStripMenuItem item_stun = new ToolStripMenuItem("STUN (" stunResult.NetType ")");
item_stun.Name = "stun";
((ToolStripDropDownButton)m_pToolbar.Items["nat"]).DropDownItems.Add(item_stun);
}
if(m_pUPnP.IsSupported){
ToolStripMenuItem item_upnp = new ToolStripMenuItem("UPnP");
item_upnp.Name = "upnp";
((ToolStripDropDownButton)m_pToolbar.Items["nat"]).DropDownItems.Add(item_upnp);
}
else{
ToolStripMenuItem item_upnp = new ToolStripMenuItem("UPnP Not Supported");
item_upnp.Name = "upnp";
item_upnp.Enabled = false;
((ToolStripDropDownButton)m_pToolbar.Items["nat"]).DropDownItems.Add(item_upnp);
}
//if(!((ToolStripDropDownButton)m_pToolbar.Items["nat"]).DropDownItems["stun"].Enabled && !((ToolStripDropDownButton)m_pToolbar.Items["nat"]).DropDownItems["upnp"].Enabled){
// MessageBox.Show("Calling may not possible, your firewall or router blocks STUN and doesn't support UPnP.\r\n\r\nSTUN Net Type: " stunResult.NetType "\r\n\r\nUPnP Supported: " m_pUPnP.IsSupported,"Error:",MessageBoxButtons.OK,MessageBoxIcon.Error);
//}
ToolStripMenuItem item_no_nat = new ToolStripMenuItem("No NAT handling");
item_no_nat.Name = "no_nat";
((ToolStripDropDownButton)m_pToolbar.Items["nat"]).DropDownItems.Add(item_no_nat);
// Select first enabled item.
foreach(ToolStripItem it in ((ToolStripDropDownButton)m_pToolbar.Items["nat"]).DropDownItems){
if(it.Enabled){
((ToolStripMenuItem)it).Checked = true;
m_NatHandlingType = it.Name;
break;
}
}
#endregion
m_pStack.BindInfo = Settings.Bindings;
m_pStack.RequestReceived = new EventHandler<SIP_RequestReceivedEventArgs>(m_pStack_RequestReceived);
// TODO:
//m_pStack.Error
m_pStack.Credentials.AddRange(this.Settings.Credentials.ToArray());
m_pStack.Start();
foreach(Account account in this.Settings.Accounts){
if(account.Register){
SIP_Uri registrarServer = SIP_Uri.Parse(account.RegistrarServer);
SIP_UA_Registration registration = m_pStack.CreateRegistration(
registrarServer,
account.AOR,
AbsoluteUri.Parse(registrarServer.Scheme ":" account.AOR.Split('@')[0] "@172.17.17.131"),
account.RegisterInterval
);
registration.StateChanged = new EventHandler(Registration_StateChanged);
registration.BeginRegister(true);
// TODO: If TLS supported, add TLS registration too.
}
m_pAccounts.Items.Add(new WComboBoxItem(account.AOR,account));
}
if(m_pAccounts.Items.Count > 0){
m_pAccounts.SelectedIndex = 0;
}
}
#endregion
#region method Restart
/// <summary>
/// Starts/restarts SIP UA.
/// </summary>
private void Restart()
{
// TODO:
}
#endregion
#region method OnError
/// <summary>
/// This method is called when unhandled exception occured.
/// </summary>
/// <param name="x">Exception.</param>
private void OnError(Exception x)
{
// TODO: FIX ME:
Console.WriteLine("Error:" x.ToString());
}
#endregion
#region method CanSupportMedia
/// <summary>
/// Checks if we can support the specified media.
/// </summary>
/// <param name="media">SDP media.</param>
/// <returns>Returns true if we can support this media, otherwise false.</returns>
/// <exception cref="ArgumentNullException">Is raised when <b>media</b> is null reference.</exception>
internal bool CanSupportMedia(SDP_MediaDescription media)
{
if(media == null){
throw new ArgumentNullException("media");
}
if(!string.Equals(media.MediaType,SDP_MediaTypes.audio,StringComparison.InvariantCultureIgnoreCase)){
return false;
}
if(!string.Equals(media.Protocol,"RTP/AVP",StringComparison.InvariantCultureIgnoreCase)){
return false;
}
if(GetOurSupportedAudioCodecs(media).Count > 0){
return true;
}
return false;
}
#endregion
#region method GetOurSupportedAudioCodecs
/// <summary>
/// Gets audio codecs which we can support from SDP media stream.
/// </summary>
/// <param name="media">SDP media stream.</param>
/// <exception cref="ArgumentNullException">Is raised when <b>media</b> is null reference.</exception>
/// <returns>Returns audio codecs which support.</returns>
internal Dictionary<int,AudioCodec> GetOurSupportedAudioCodecs(SDP_MediaDescription media)
{
if(media == null){
throw new ArgumentNullException("media");
}
Dictionary<int,AudioCodec> codecs = new Dictionary<int,AudioCodec>();
// Check for IANA registered payload. Custom range is 96-127 and always must have rtpmap attribute.
foreach(string format in media.MediaFormats){
int payload = Convert.ToInt32(format);
if(payload < 96 && m_pAudioCodecs.ContainsKey(payload)){
if(!codecs.ContainsKey(payload)){
codecs.Add(payload,m_pAudioCodecs[payload]);
}
}
}
// Check rtpmap payloads.
foreach(SDP_Attribute a in media.Attributes){
if(string.Equals(a.Name,"rtpmap",StringComparison.InvariantCultureIgnoreCase)){
// Example: 0 PCMU/8000
string[] parts = a.Value.Split(' ');
int payload = Convert.ToInt32(parts[0]);
string codecName = parts[1].Split('/')[0];
foreach(AudioCodec codec in m_pAudioCodecs.Values){
if(string.Equals(codec.Name,codecName,StringComparison.InvariantCultureIgnoreCase)){
if(!codecs.ContainsKey(payload)){
codecs.Add(payload,codec);
}
}
}
}
}
return codecs;
}
#endregion
#region method CreateRtpSession
/// <summary>
/// Creates new RTP session.
/// </summary>
/// <param name="rtpMultimediaSession">RTP multimedia session.</param>
/// <returns>Returns created RTP session or null if failed to create RTP session.</returns>
/// <exception cref="ArgumentNullException">Is raised <b>rtpMultimediaSession</b> is null reference.</exception>
internal RTP_Session CreateRtpSession(RTP_MultimediaSession rtpMultimediaSession)
{
if(rtpMultimediaSession == null){
throw new ArgumentNullException("rtpMultimediaSession");
}
//--- Search RTP IP -------------------------------------------------------//
IPAddress rtpIP = null;
foreach(IPAddress ip in Dns.GetHostAddresses("")){
if(ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork){
rtpIP = ip;
break;
}
}
if(rtpIP == null){
throw new Exception("None of the network connection is available.");
}
//------------------------------------------------------------------------//
// Search free ports for RTP session.
for(int i=0;i<100;i =2){
try{
return rtpMultimediaSession.CreateSession(new RTP_Address(rtpIP,m_RtpBasePort,m_RtpBasePort 1),new RTP_Clock(1,8000));
}
catch{
m_RtpBasePort = 2;
}
}
return null;
}
#endregion
#region method HandleNAT
/// <summary>
/// Handles NAT and stores RTP data to <b>mediaStream</b>.
/// </summary>
/// <param name="mediaStream">SDP media stream.</param>
/// <param name="rtpSession">RTP session.</param>
/// <returns>Returns true if NAT handled ok, otherwise false.</returns>
internal bool HandleNAT(SDP_MediaDescription mediaStream,RTP_Session rtpSession)
{
if(mediaStream == null){
throw new ArgumentNullException("mediaStream");
}
if(rtpSession == null){
throw new ArgumentNullException("rtpSession");
}
IPEndPoint rtpPublicEP = null;
IPEndPoint rtcpPublicEP = null;
// We have public IP.
if(!Net_Utils.IsPrivateIP(rtpSession.LocalEP.IP)){
rtpPublicEP = rtpSession.LocalEP.RtpEP;
rtcpPublicEP = rtpSession.LocalEP.RtcpEP;
}
// No NAT handling.
else if(m_NatHandlingType == "no_nat"){
rtpPublicEP = rtpSession.LocalEP.RtpEP;
rtcpPublicEP = rtpSession.LocalEP.RtcpEP;
}
// Use STUN.
else if(m_NatHandlingType == "stun"){
rtpSession.StunPublicEndPoints(m_StunServer,3478,out rtpPublicEP,out rtcpPublicEP);
}
// Use UPnP.
else if(m_NatHandlingType == "upnp"){
// Try to open UPnP ports.
if(m_pUPnP.IsSupported){
int rtpPublicPort = rtpSession.LocalEP.RtpEP.Port;
int rtcpPublicPort = rtpSession.LocalEP.RtcpEP.Port;
try{
UPnP_NAT_Map[] maps = m_pUPnP.GetPortMappings();
while(true){
bool conficts = false;
// Check that some other application doesn't use that port.
foreach(UPnP_NAT_Map map in maps){
// Existing map entry conflicts.
if(Convert.ToInt32(map.ExternalPort) == rtpPublicPort || Convert.ToInt32(map.ExternalPort) == rtcpPublicPort){
rtpPublicPort = 2;
rtcpPublicPort = 2;
conficts = true;
break;
}
}
if(!conficts){
break;
}
}
m_pUPnP.AddPortMapping(true,"LS RTP","UDP",null,rtpPublicPort,rtpSession.LocalEP.RtpEP,0);
m_pUPnP.AddPortMapping(true,"LS RTCP","UDP",null,rtcpPublicPort,rtpSession.LocalEP.RtcpEP,0);
IPAddress publicIP = m_pUPnP.GetExternalIPAddress();
rtpPublicEP = new IPEndPoint(publicIP,rtpPublicPort);
rtcpPublicEP = new IPEndPoint(publicIP,rtcpPublicPort);
mediaStream.Tags.Add("upnp_rtp_map",new UPnP_NAT_Map(true,"UDP","",rtpPublicPort.ToString(),rtpSession.LocalEP.IP.ToString(),rtpSession.LocalEP.RtpEP.Port,"LS RTP",0));
mediaStream.Tags.Add("upnp_rtcp_map",new UPnP_NAT_Map(true,"UDP","",rtcpPublicPort.ToString(),rtpSession.LocalEP.IP.ToString(),rtpSession.LocalEP.RtcpEP.Port,"LS RTCP",0));
}
catch{
}
}
}
if(rtpPublicEP != null && rtcpPublicEP != null){
mediaStream.Port = rtpPublicEP.Port;
if((rtpPublicEP.Port 1) != rtcpPublicEP.Port){
// Remove old rport attribute, if any.
for(int i=0;i<mediaStream.Attributes.Count;i ){
if(string.Equals(mediaStream.Attributes[i].Name,"rport",StringComparison.InvariantCultureIgnoreCase)){
mediaStream.Attributes.RemoveAt(i);
i--;
}
}
mediaStream.Attributes.Add(new SDP_Attribute("rport",rtcpPublicEP.Port.ToString()));
}
mediaStream.Connection = new SDP_Connection("IN","IP4",rtpPublicEP.Address.ToString());
return true;
}
return false;
}
#endregion
#region Properties implementation
/// <summary>
/// Gets UA settings.
/// </summary>
public Settings Settings
{
get{ return m_pSettings; }
}
/// <summary>
/// Gets SIP UA.
/// </summary>
public SIP_Stack SIP_Stack
{
get{ return m_pStack; }
}
/// <summary>
/// Gets audio codecs.
/// </summary>
public Dictionary<int,AudioCodec> AudioCodecs
{
get{ return m_pAudioCodecs; }
}
/// <summary>
/// Gets call list.
/// </summary>
public wctrl_Call[] Calls
{
get{ return m_pCallTab_Calls_Calls.Calls; }
}
/// <summary>
/// Gets if debug mode.
/// </summary>
internal bool IsDebug
{
get{ return m_IsDebug; }
}
/// <summary>
/// Gets UPnP NAT client.
/// </summary>
internal UPnP_NAT_Client UPnP
{
get{ return m_pUPnP; }
}
#endregion
}
}
标签: C#
小贴士
感谢您为本站写下的评论,您的评论对其它用户来说具有重要的参考价值,所以请认真填写。
- 类似“顶”、“沙发”之类没有营养的文字,对勤劳贡献的楼主来说是令人沮丧的反馈信息。
- 相信您也不想看到一排文字/表情墙,所以请不要反馈意义不大的重复字符,也请尽量不要纯表情的回复。
- 提问之前请再仔细看一遍楼主的说明,或许是您遗漏了。
- 请勿到处挖坑绊人、招贴广告。既占空间让人厌烦,又没人会搭理,于人于己都无利。
关于好例子网
本站旨在为广大IT学习爱好者提供一个非营利性互相学习交流分享平台。本站所有资源都可以被免费获取学习研究。本站资源来自网友分享,对搜索内容的合法性不具有预见性、识别性、控制性,仅供学习研究,请务必在下载后24小时内给予删除,不得用于其他任何用途,否则后果自负。基于互联网的特殊性,平台无法对用户传输的作品、信息、内容的权属或合法性、安全性、合规性、真实性、科学性、完整权、有效性等进行实质审查;无论平台是否已进行审查,用户均应自行承担因其传输的作品、信息、内容而可能或已经产生的侵权或权属纠纷等法律责任。本站所有资源不代表本站的观点或立场,基于网友分享,根据中国法律《信息网络传播权保护条例》第二十二与二十三条之规定,若资源存在侵权或相关问题请联系本站客服人员,点此联系我们。关于更多版权及免责申明参见 版权及免责申明


网友评论
我要评论