Added Opus.Net, fixed public IP method, starting to add outgoing voice support.
This commit is contained in:
@@ -141,6 +141,12 @@
|
|||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Opus.Net.Net45\Opus.Net.csproj">
|
||||||
|
<Project>{114c8c10-7354-4ec3-819a-33e83aa57768}</Project>
|
||||||
|
<Name>Opus.Net</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<Import Project="..\..\packages\Baseclass.Contrib.Nuget.Output.1.0.0\build\net40\Baseclass.Contrib.Nuget.Output.targets" Condition="Exists('..\..\packages\Baseclass.Contrib.Nuget.Output.1.0.0\build\net40\Baseclass.Contrib.Nuget.Output.targets')" />
|
<Import Project="..\..\packages\Baseclass.Contrib.Nuget.Output.1.0.0\build\net40\Baseclass.Contrib.Nuget.Output.targets" Condition="Exists('..\..\packages\Baseclass.Contrib.Nuget.Output.1.0.0\build\net40\Baseclass.Contrib.Nuget.Output.targets')" />
|
||||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||||
|
|||||||
@@ -1320,6 +1320,13 @@ namespace Discord
|
|||||||
return DiscordAPI.Undeafen(serverId, userId);
|
return DiscordAPI.Undeafen(serverId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !DNXCORE50
|
||||||
|
public void SendVoiceWAV(byte[] buffer, int count)
|
||||||
|
{
|
||||||
|
_voiceWebSocket.SendWAV(buffer, count);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
//Profile
|
//Profile
|
||||||
/// <summary> Changes your username to newName. </summary>
|
/// <summary> Changes your username to newName. </summary>
|
||||||
public async Task ChangeUsername(string newName, string currentEmail, string currentPassword)
|
public async Task ChangeUsername(string newName, string currentEmail, string currentPassword)
|
||||||
@@ -1360,7 +1367,7 @@ namespace Discord
|
|||||||
private string GenerateNonce()
|
private string GenerateNonce()
|
||||||
{
|
{
|
||||||
lock (_rand)
|
lock (_rand)
|
||||||
return _rand.Next(0, int.MaxValue).ToString();
|
return _rand.Next().ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary>
|
/// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary>
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ using System.Net.Sockets;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using WebSocketMessage = Discord.API.Models.VoiceWebSocketCommands.WebSocketMessage;
|
using WebSocketMessage = Discord.API.Models.VoiceWebSocketCommands.WebSocketMessage;
|
||||||
|
using System.Text;
|
||||||
|
#if !DNXCORE50
|
||||||
|
using Opus.Net;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace Discord
|
namespace Discord
|
||||||
{
|
{
|
||||||
@@ -23,13 +27,24 @@ namespace Discord
|
|||||||
private byte[] _secretKey;
|
private byte[] _secretKey;
|
||||||
private string _mode;
|
private string _mode;
|
||||||
private bool _isFirst;
|
private bool _isFirst;
|
||||||
|
private ushort _sequence;
|
||||||
|
private uint _ssrc;
|
||||||
|
private long _startTicks;
|
||||||
|
private readonly Random _rand = new Random();
|
||||||
|
|
||||||
|
#if !DNXCORE50
|
||||||
|
private OpusEncoder _encoder;
|
||||||
|
#endif
|
||||||
|
|
||||||
public DiscordVoiceSocket(int timeout, int interval)
|
public DiscordVoiceSocket(int timeout, int interval)
|
||||||
: base(timeout, interval)
|
: base(timeout, interval)
|
||||||
{
|
{
|
||||||
_connectWaitOnLogin = new ManualResetEventSlim(false);
|
_connectWaitOnLogin = new ManualResetEventSlim(false);
|
||||||
_sendQueue = new ConcurrentQueue<byte[]>();
|
_sendQueue = new ConcurrentQueue<byte[]>();
|
||||||
}
|
#if !DNXCORE50
|
||||||
|
_encoder = OpusEncoder.Create(24000, 1, Application.Voip);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnConnect()
|
protected override void OnConnect()
|
||||||
{
|
{
|
||||||
@@ -58,7 +73,7 @@ namespace Discord
|
|||||||
|
|
||||||
_connectWaitOnLogin.Reset();
|
_connectWaitOnLogin.Reset();
|
||||||
|
|
||||||
_myIp = (await Http.Get("http://ipinfo.io/ip")).Trim();
|
_sequence = 0;
|
||||||
|
|
||||||
VoiceWebSocketCommands.Login msg = new VoiceWebSocketCommands.Login();
|
VoiceWebSocketCommands.Login msg = new VoiceWebSocketCommands.Login();
|
||||||
msg.Payload.ServerId = serverId;
|
msg.Payload.ServerId = serverId;
|
||||||
@@ -138,13 +153,18 @@ namespace Discord
|
|||||||
//_mode = payload.Modes.LastOrDefault();
|
//_mode = payload.Modes.LastOrDefault();
|
||||||
_mode = "plain";
|
_mode = "plain";
|
||||||
_udp.Connect(_endpoint);
|
_udp.Connect(_endpoint);
|
||||||
var ssrc = payload.SSRC;
|
lock(_rand)
|
||||||
|
{
|
||||||
|
_sequence = (ushort)_rand.Next(0, ushort.MaxValue);
|
||||||
|
_startTicks = DateTime.UtcNow.Ticks - _rand.Next();
|
||||||
|
}
|
||||||
|
_ssrc = payload.SSRC;
|
||||||
|
|
||||||
_sendQueue.Enqueue(new byte[70] {
|
_sendQueue.Enqueue(new byte[70] {
|
||||||
(byte)((ssrc >> 24) & 0xFF),
|
(byte)((_ssrc >> 24) & 0xFF),
|
||||||
(byte)((ssrc >> 16) & 0xFF),
|
(byte)((_ssrc >> 16) & 0xFF),
|
||||||
(byte)((ssrc >> 8) & 0xFF),
|
(byte)((_ssrc >> 8) & 0xFF),
|
||||||
(byte)((ssrc >> 0) & 0xFF),
|
(byte)((_ssrc >> 0) & 0xFF),
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
@@ -179,6 +199,8 @@ namespace Discord
|
|||||||
|
|
||||||
int port = buffer[68] | buffer[69] << 8;
|
int port = buffer[68] | buffer[69] << 8;
|
||||||
|
|
||||||
|
_myIp = Encoding.ASCII.GetString(buffer, 4, 70 - 6).TrimEnd('\0');
|
||||||
|
|
||||||
var login2 = new VoiceWebSocketCommands.Login2();
|
var login2 = new VoiceWebSocketCommands.Login2();
|
||||||
login2.Payload.Protocol = "udp";
|
login2.Payload.Protocol = "udp";
|
||||||
login2.Payload.SocketData.Address = _myIp;
|
login2.Payload.SocketData.Address = _myIp;
|
||||||
@@ -189,7 +211,7 @@ namespace Discord
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
//Parse RTP Data
|
//Parse RTP Data
|
||||||
if (length < 12)
|
/*if (length < 12)
|
||||||
throw new Exception($"Unexpected message length. Expected >= 12, got {length}.");
|
throw new Exception($"Unexpected message length. Expected >= 12, got {length}.");
|
||||||
|
|
||||||
byte flags = buffer[0];
|
byte flags = buffer[0];
|
||||||
@@ -227,11 +249,38 @@ namespace Discord
|
|||||||
byte[] newBuffer = new byte[buffer.Length - 12];
|
byte[] newBuffer = new byte[buffer.Length - 12];
|
||||||
Buffer.BlockCopy(buffer, 12, newBuffer, 0, newBuffer.Length);
|
Buffer.BlockCopy(buffer, 12, newBuffer, 0, newBuffer.Length);
|
||||||
buffer = newBuffer;
|
buffer = newBuffer;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
//TODO: Use Voice Data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !DNXCORE50
|
||||||
|
public void SendWAV(byte[] buffer, int count)
|
||||||
|
{
|
||||||
|
int encodedLength;
|
||||||
|
buffer = _encoder.Encode(buffer, count, out encodedLength);
|
||||||
|
byte[] packet = new byte[12 + encodedLength];
|
||||||
|
Buffer.BlockCopy(buffer, 0, packet, 12, encodedLength);
|
||||||
|
|
||||||
|
ushort sequence = _sequence++;
|
||||||
|
long timestamp = (DateTime.UtcNow.Ticks - _startTicks) >> 2; //200ns resolution
|
||||||
|
packet[0] = 0x80; //Flags;
|
||||||
|
packet[1] = 0x78; //Payload Type
|
||||||
|
packet[2] = (byte)((sequence >> 8) & 0xFF);
|
||||||
|
packet[3] = (byte)((sequence >> 0) & 0xFF);
|
||||||
|
packet[4] = (byte)((timestamp >> 24) & 0xFF);
|
||||||
|
packet[5] = (byte)((timestamp >> 16) & 0xFF);
|
||||||
|
packet[6] = (byte)((timestamp >> 8) & 0xFF);
|
||||||
|
packet[7] = (byte)((timestamp >> 0) & 0xFF);
|
||||||
|
packet[8] = (byte)((_ssrc >> 24) & 0xFF);
|
||||||
|
packet[9] = (byte)((_ssrc >> 16) & 0xFF);
|
||||||
|
packet[10] = (byte)((_ssrc >> 8) & 0xFF);
|
||||||
|
packet[11] = (byte)((_ssrc >> 0) & 0xFF);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
protected override object GetKeepAlive()
|
protected override object GetKeepAlive()
|
||||||
{
|
{
|
||||||
return new VoiceWebSocketCommands.KeepAlive();
|
return new VoiceWebSocketCommands.KeepAlive();
|
||||||
|
|||||||
@@ -1,54 +1,56 @@
|
|||||||
{
|
{
|
||||||
"version": "0.5.0-*",
|
"version": "0.5.0-*",
|
||||||
"description": "An unofficial .Net API wrapper for the Discord client.",
|
"description": "An unofficial .Net API wrapper for the Discord client.",
|
||||||
"authors": [ "RogueException" ],
|
"authors": [ "RogueException" ],
|
||||||
"tags": [ "discord", "discordapp" ],
|
"tags": [ "discord", "discordapp" ],
|
||||||
"projectUrl": "https://github.com/RogueException/Discord.Net",
|
"projectUrl": "https://github.com/RogueException/Discord.Net",
|
||||||
"licenseUrl": "http://opensource.org/licenses/MIT",
|
"licenseUrl": "http://opensource.org/licenses/MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/RogueException/Discord.Net"
|
"url": "git://github.com/RogueException/Discord.Net"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"FullDebug": {
|
"FullDebug": {
|
||||||
"compilationOptions": {
|
"compilationOptions": {
|
||||||
"define": [ "DEBUG", "TRACE", "TEST_RESPONSES" ]
|
"define": [ "DEBUG", "TRACE", "TEST_RESPONSES" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Newtonsoft.Json": "7.0.1"
|
"Newtonsoft.Json": "7.0.1"
|
||||||
},
|
},
|
||||||
|
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
"net45": {
|
"net45": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Net.Http": "2.2.22",
|
"Microsoft.Net.Http": "2.2.22",
|
||||||
"libsodium-net": "0.8.0",
|
"libsodium-net": "0.8.0",
|
||||||
"Baseclass.Contrib.Nuget.Output": "2.1.0"
|
"Baseclass.Contrib.Nuget.Output": "2.1.0",
|
||||||
}
|
"Opus.Net": "0.1.0"
|
||||||
},
|
}
|
||||||
"dnx451": {
|
},
|
||||||
"dependencies": {
|
"dnx451": {
|
||||||
"Microsoft.Net.Http": "2.2.22",
|
"dependencies": {
|
||||||
"libsodium-net": "0.8.0",
|
"Microsoft.Net.Http": "2.2.22",
|
||||||
"Baseclass.Contrib.Nuget.Output": "2.1.0"
|
"libsodium-net": "0.8.0",
|
||||||
}
|
"Baseclass.Contrib.Nuget.Output": "2.1.0",
|
||||||
},
|
"Opus.Net": "0.1.0"
|
||||||
"dnxcore50": {
|
}
|
||||||
"dependencies": {
|
},
|
||||||
"System.Collections.Concurrent": "4.0.10",
|
"dnxcore50": {
|
||||||
"System.Diagnostics.Debug": "4.0.10",
|
"dependencies": {
|
||||||
"System.IO.Compression": "4.0.0",
|
"System.Collections.Concurrent": "4.0.10",
|
||||||
"System.Linq": "4.0.0",
|
"System.Diagnostics.Debug": "4.0.10",
|
||||||
"System.Net.Requests": "4.0.10",
|
"System.IO.Compression": "4.0.0",
|
||||||
"System.Net.Sockets": "4.0.10-beta-23019",
|
"System.Linq": "4.0.0",
|
||||||
"System.Net.WebSockets.Client": "4.0.0-beta-23123",
|
"System.Net.Requests": "4.0.10",
|
||||||
"System.Runtime": "4.0.20",
|
"System.Net.Sockets": "4.0.10-beta-23019",
|
||||||
"System.Text.RegularExpressions": "4.0.10",
|
"System.Net.WebSockets.Client": "4.0.0-beta-23123",
|
||||||
"System.Net.NameResolution": "4.0.0-beta-23019"
|
"System.Runtime": "4.0.20",
|
||||||
}
|
"System.Text.RegularExpressions": "4.0.10",
|
||||||
}
|
"System.Net.NameResolution": "4.0.0-beta-23019"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
src/Opus.Net.Net45/Opus.Net.csproj
Normal file
79
src/Opus.Net.Net45/Opus.Net.csproj
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{114C8C10-7354-4EC3-819A-33E83AA57768}</ProjectGuid>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<RootNamespace>Discord.Net</RootNamespace>
|
||||||
|
<AssemblyName>Discord.Net</AssemblyName>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||||
|
<NuGetPackageImportStamp>
|
||||||
|
</NuGetPackageImportStamp>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>2</WarningLevel>
|
||||||
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Newtonsoft.Json, Version=7.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||||
|
<Private>True</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Sodium, Version=0.8.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\..\packages\libsodium-net.0.8.0\lib\Net40\Sodium.dll</HintPath>
|
||||||
|
<Private>True</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\Opus.Net\API.cs">
|
||||||
|
<Link>API.cs</Link>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="..\Opus.Net\OpusDecoder.cs">
|
||||||
|
<Link>OpusDecoder.cs</Link>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="..\Opus.Net\OpusEncoder.cs">
|
||||||
|
<Link>OpusEncoder.cs</Link>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
<Import Project="..\..\packages\Baseclass.Contrib.Nuget.Output.1.0.0\build\net40\Baseclass.Contrib.Nuget.Output.targets" Condition="Exists('..\..\packages\Baseclass.Contrib.Nuget.Output.1.0.0\build\net40\Baseclass.Contrib.Nuget.Output.targets')" />
|
||||||
|
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Error Condition="!Exists('..\..\packages\Baseclass.Contrib.Nuget.Output.1.0.0\build\net40\Baseclass.Contrib.Nuget.Output.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Baseclass.Contrib.Nuget.Output.1.0.0\build\net40\Baseclass.Contrib.Nuget.Output.targets'))" />
|
||||||
|
</Target>
|
||||||
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
|
Other similar extension points exist, see Microsoft.Common.targets.
|
||||||
|
<Target Name="BeforeBuild">
|
||||||
|
</Target>
|
||||||
|
<Target Name="AfterBuild">
|
||||||
|
</Target>
|
||||||
|
-->
|
||||||
|
</Project>
|
||||||
17
src/Opus.Net.Net45/Properties/AssemblyInfo.cs
Normal file
17
src/Opus.Net.Net45/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
[assembly: AssemblyTitle("Opus.Net")]
|
||||||
|
[assembly: AssemblyDescription("Opus .NET Wrapper")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("John Carruthers")]
|
||||||
|
[assembly: AssemblyProduct("Opus.Net")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © 2013")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")]
|
||||||
|
|
||||||
|
[assembly: AssemblyVersion("0.1.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("0.1.0.0")]
|
||||||
6
src/Opus.Net.Net45/packages.config
Normal file
6
src/Opus.Net.Net45/packages.config
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="Baseclass.Contrib.Nuget.Output" version="1.0.0" targetFramework="net45" />
|
||||||
|
<package id="libsodium-net" version="0.8.0" targetFramework="net45" />
|
||||||
|
<package id="Newtonsoft.Json" version="7.0.1" targetFramework="net45" />
|
||||||
|
</packages>
|
||||||
108
src/Opus.Net/API.cs
Normal file
108
src/Opus.Net/API.cs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Opus.Net
|
||||||
|
{
|
||||||
|
internal class API
|
||||||
|
{
|
||||||
|
static API()
|
||||||
|
{
|
||||||
|
if (LoadLibrary(Environment.Is64BitProcess ? "lib/x64/opus.dll" : "lib/x86/opus.dll") == IntPtr.Zero)
|
||||||
|
throw new FileNotFoundException("Unable to find opus.dll", "opus.dll");
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern IntPtr LoadLibrary(string dllToLoad);
|
||||||
|
|
||||||
|
[DllImport("opus.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
internal static extern IntPtr opus_encoder_create(int Fs, int channels, int application, out IntPtr error);
|
||||||
|
|
||||||
|
[DllImport("opus.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
internal static extern void opus_encoder_destroy(IntPtr encoder);
|
||||||
|
|
||||||
|
[DllImport("opus.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
internal static extern int opus_encode(IntPtr st, byte[] pcm, int frame_size, IntPtr data, int max_data_bytes);
|
||||||
|
|
||||||
|
[DllImport("opus.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
internal static extern IntPtr opus_decoder_create(int Fs, int channels, out IntPtr error);
|
||||||
|
|
||||||
|
[DllImport("opus.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
internal static extern void opus_decoder_destroy(IntPtr decoder);
|
||||||
|
|
||||||
|
[DllImport("opus.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
internal static extern int opus_decode(IntPtr st, byte[] data, int len, IntPtr pcm, int frame_size, int decode_fec);
|
||||||
|
|
||||||
|
[DllImport("opus.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
internal static extern int opus_encoder_ctl(IntPtr st, Ctl request, int value);
|
||||||
|
|
||||||
|
[DllImport("opus.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
internal static extern int opus_encoder_ctl(IntPtr st, Ctl request, out int value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Ctl : int
|
||||||
|
{
|
||||||
|
SetBitrateRequest = 4002,
|
||||||
|
GetBitrateRequest = 4003,
|
||||||
|
SetInbandFECRequest = 4012,
|
||||||
|
GetInbandFECRequest = 4013
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supported coding modes.
|
||||||
|
/// </summary>
|
||||||
|
public enum Application
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Best for most VoIP/videoconference applications where listening quality and intelligibility matter most.
|
||||||
|
/// </summary>
|
||||||
|
Voip = 2048,
|
||||||
|
/// <summary>
|
||||||
|
/// Best for broadcast/high-fidelity application where the decoded audio should be as close as possible to input.
|
||||||
|
/// </summary>
|
||||||
|
Audio = 2049,
|
||||||
|
/// <summary>
|
||||||
|
/// Only use when lowest-achievable latency is what matters most. Voice-optimized modes cannot be used.
|
||||||
|
/// </summary>
|
||||||
|
Restricted_LowLatency = 2051
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Errors
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No error.
|
||||||
|
/// </summary>
|
||||||
|
OK = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// One or more invalid/out of range arguments.
|
||||||
|
/// </summary>
|
||||||
|
BadArg = -1,
|
||||||
|
/// <summary>
|
||||||
|
/// The mode struct passed is invalid.
|
||||||
|
/// </summary>
|
||||||
|
BufferToSmall = -2,
|
||||||
|
/// <summary>
|
||||||
|
/// An internal error was detected.
|
||||||
|
/// </summary>
|
||||||
|
InternalError = -3,
|
||||||
|
/// <summary>
|
||||||
|
/// The compressed data passed is corrupted.
|
||||||
|
/// </summary>
|
||||||
|
InvalidPacket = -4,
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid/unsupported request number.
|
||||||
|
/// </summary>
|
||||||
|
Unimplemented = -5,
|
||||||
|
/// <summary>
|
||||||
|
/// An encoder or decoder structure is invalid or already freed.
|
||||||
|
/// </summary>
|
||||||
|
InvalidState = -6,
|
||||||
|
/// <summary>
|
||||||
|
/// Memory allocation has failed.
|
||||||
|
/// </summary>
|
||||||
|
AllocFail = -7
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/Opus.Net/Opus.Net.xproj
Normal file
20
src/Opus.Net/Opus.Net.xproj
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||||
|
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
<PropertyGroup Label="Globals">
|
||||||
|
<ProjectGuid>42ab6a2d-2f2c-4003-80ef-33b5b5b0ed8e</ProjectGuid>
|
||||||
|
<RootNamespace>Opus.Net</RootNamespace>
|
||||||
|
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||||
|
<OutputPath Condition="'$(OutputPath)'=='' ">..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
</Project>
|
||||||
133
src/Opus.Net/OpusDecoder.cs
Normal file
133
src/Opus.Net/OpusDecoder.cs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Opus.Net
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Opus audio decoder.
|
||||||
|
/// </summary>
|
||||||
|
public class OpusDecoder : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new Opus decoder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="outputSampleRate">Sample rate to decode at (Hz). This must be one of 8000, 12000, 16000, 24000, or 48000.</param>
|
||||||
|
/// <param name="outputChannels">Number of channels to decode.</param>
|
||||||
|
/// <returns>A new <c>OpusDecoder</c>.</returns>
|
||||||
|
public static OpusDecoder Create(int outputSampleRate, int outputChannels)
|
||||||
|
{
|
||||||
|
if (outputSampleRate != 8000 &&
|
||||||
|
outputSampleRate != 12000 &&
|
||||||
|
outputSampleRate != 16000 &&
|
||||||
|
outputSampleRate != 24000 &&
|
||||||
|
outputSampleRate != 48000)
|
||||||
|
throw new ArgumentOutOfRangeException("inputSamplingRate");
|
||||||
|
if (outputChannels != 1 && outputChannels != 2)
|
||||||
|
throw new ArgumentOutOfRangeException("inputChannels");
|
||||||
|
|
||||||
|
IntPtr error;
|
||||||
|
IntPtr decoder = API.opus_decoder_create(outputSampleRate, outputChannels, out error);
|
||||||
|
if ((Errors)error != Errors.OK)
|
||||||
|
{
|
||||||
|
throw new Exception("Exception occured while creating decoder");
|
||||||
|
}
|
||||||
|
return new OpusDecoder(decoder, outputSampleRate, outputChannels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr _decoder;
|
||||||
|
|
||||||
|
private OpusDecoder(IntPtr decoder, int outputSamplingRate, int outputChannels)
|
||||||
|
{
|
||||||
|
_decoder = decoder;
|
||||||
|
OutputSamplingRate = outputSamplingRate;
|
||||||
|
OutputChannels = outputChannels;
|
||||||
|
MaxDataBytes = 4000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Produces PCM samples from Opus encoded data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inputOpusData">Opus encoded data to decode, null for dropped packet.</param>
|
||||||
|
/// <param name="dataLength">Length of data to decode.</param>
|
||||||
|
/// <param name="decodedLength">Set to the length of the decoded sample data.</param>
|
||||||
|
/// <returns>PCM audio samples.</returns>
|
||||||
|
public unsafe byte[] Decode(byte[] inputOpusData, int dataLength, out int decodedLength)
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
throw new ObjectDisposedException("OpusDecoder");
|
||||||
|
|
||||||
|
IntPtr decodedPtr;
|
||||||
|
byte[] decoded = new byte[MaxDataBytes];
|
||||||
|
int frameCount = FrameCount(MaxDataBytes);
|
||||||
|
int length = 0;
|
||||||
|
fixed (byte* bdec = decoded)
|
||||||
|
{
|
||||||
|
decodedPtr = new IntPtr((void*)bdec);
|
||||||
|
|
||||||
|
if (inputOpusData != null)
|
||||||
|
length = API.opus_decode(_decoder, inputOpusData, dataLength, decodedPtr, frameCount, 0);
|
||||||
|
else
|
||||||
|
length = API.opus_decode(_decoder, null, 0, decodedPtr, frameCount, (ForwardErrorCorrection) ? 1 : 0);
|
||||||
|
}
|
||||||
|
decodedLength = length * 2;
|
||||||
|
if (length < 0)
|
||||||
|
throw new Exception("Decoding failed - " + ((Errors)length).ToString());
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the number of frames that can fit into a buffer of the given size.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bufferSize"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int FrameCount(int bufferSize)
|
||||||
|
{
|
||||||
|
// seems like bitrate should be required
|
||||||
|
int bitrate = 16;
|
||||||
|
int bytesPerSample = (bitrate / 8) * OutputChannels;
|
||||||
|
return bufferSize / bytesPerSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the output sampling rate of the decoder.
|
||||||
|
/// </summary>
|
||||||
|
public int OutputSamplingRate { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of channels of the decoder.
|
||||||
|
/// </summary>
|
||||||
|
public int OutputChannels { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the size of memory allocated for decoding data.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxDataBytes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether forward error correction is enabled or not.
|
||||||
|
/// </summary>
|
||||||
|
public bool ForwardErrorCorrection { get; set; }
|
||||||
|
|
||||||
|
~OpusDecoder()
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool disposed;
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
|
||||||
|
if (_decoder != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
API.opus_decoder_destroy(_decoder);
|
||||||
|
_decoder = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
198
src/Opus.Net/OpusEncoder.cs
Normal file
198
src/Opus.Net/OpusEncoder.cs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Opus.Net
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Opus codec wrapper.
|
||||||
|
/// </summary>
|
||||||
|
public class OpusEncoder : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new Opus encoder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inputSamplingRate">Sampling rate of the input signal (Hz). This must be one of 8000, 12000, 16000, 24000, or 48000.</param>
|
||||||
|
/// <param name="inputChannels">Number of channels (1 or 2) in input signal.</param>
|
||||||
|
/// <param name="application">Coding mode.</param>
|
||||||
|
/// <returns>A new <c>OpusEncoder</c></returns>
|
||||||
|
public static OpusEncoder Create(int inputSamplingRate, int inputChannels, Application application)
|
||||||
|
{
|
||||||
|
if (inputSamplingRate != 8000 &&
|
||||||
|
inputSamplingRate != 12000 &&
|
||||||
|
inputSamplingRate != 16000 &&
|
||||||
|
inputSamplingRate != 24000 &&
|
||||||
|
inputSamplingRate != 48000)
|
||||||
|
throw new ArgumentOutOfRangeException("inputSamplingRate");
|
||||||
|
if (inputChannels != 1 && inputChannels != 2)
|
||||||
|
throw new ArgumentOutOfRangeException("inputChannels");
|
||||||
|
|
||||||
|
IntPtr error;
|
||||||
|
IntPtr encoder = API.opus_encoder_create(inputSamplingRate, inputChannels, (int)application, out error);
|
||||||
|
if ((Errors)error != Errors.OK)
|
||||||
|
{
|
||||||
|
throw new Exception("Exception occured while creating encoder");
|
||||||
|
}
|
||||||
|
return new OpusEncoder(encoder, inputSamplingRate, inputChannels, application);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr _encoder;
|
||||||
|
|
||||||
|
private OpusEncoder(IntPtr encoder, int inputSamplingRate, int inputChannels, Application application)
|
||||||
|
{
|
||||||
|
_encoder = encoder;
|
||||||
|
InputSamplingRate = inputSamplingRate;
|
||||||
|
InputChannels = inputChannels;
|
||||||
|
Application = application;
|
||||||
|
MaxDataBytes = 4000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Produces Opus encoded audio from PCM samples.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inputPcmSamples">PCM samples to encode.</param>
|
||||||
|
/// <param name="sampleLength">How many bytes to encode.</param>
|
||||||
|
/// <param name="encodedLength">Set to length of encoded audio.</param>
|
||||||
|
/// <returns>Opus encoded audio buffer.</returns>
|
||||||
|
public unsafe byte[] Encode(byte[] inputPcmSamples, int sampleLength, out int encodedLength)
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
throw new ObjectDisposedException("OpusEncoder");
|
||||||
|
|
||||||
|
int frames = FrameCount(inputPcmSamples);
|
||||||
|
IntPtr encodedPtr;
|
||||||
|
byte[] encoded = new byte[MaxDataBytes];
|
||||||
|
int length = 0;
|
||||||
|
fixed (byte* benc = encoded)
|
||||||
|
{
|
||||||
|
encodedPtr = new IntPtr((void*)benc);
|
||||||
|
length = API.opus_encode(_encoder, inputPcmSamples, frames, encodedPtr, sampleLength);
|
||||||
|
}
|
||||||
|
encodedLength = length;
|
||||||
|
if (length < 0)
|
||||||
|
throw new Exception("Encoding failed - " + ((Errors)length).ToString());
|
||||||
|
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the number of frames in the PCM samples.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pcmSamples"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int FrameCount(byte[] pcmSamples)
|
||||||
|
{
|
||||||
|
// seems like bitrate should be required
|
||||||
|
int bitrate = 16;
|
||||||
|
int bytesPerSample = (bitrate / 8) * InputChannels;
|
||||||
|
return pcmSamples.Length / bytesPerSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to determine how many bytes are required for encoding to work.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="frameCount">Target frame size.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int FrameByteCount(int frameCount)
|
||||||
|
{
|
||||||
|
int bitrate = 16;
|
||||||
|
int bytesPerSample = (bitrate / 8) * InputChannels;
|
||||||
|
return frameCount * bytesPerSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the input sampling rate of the encoder.
|
||||||
|
/// </summary>
|
||||||
|
public int InputSamplingRate { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of channels of the encoder.
|
||||||
|
/// </summary>
|
||||||
|
public int InputChannels { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the coding mode of the encoder.
|
||||||
|
/// </summary>
|
||||||
|
public Application Application { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the size of memory allocated for reading encoded data.
|
||||||
|
/// 4000 is recommended.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxDataBytes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the bitrate setting of the encoding.
|
||||||
|
/// </summary>
|
||||||
|
public int Bitrate
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
throw new ObjectDisposedException("OpusEncoder");
|
||||||
|
int bitrate;
|
||||||
|
var ret = API.opus_encoder_ctl(_encoder, Ctl.GetBitrateRequest, out bitrate);
|
||||||
|
if (ret < 0)
|
||||||
|
throw new Exception("Encoder error - " + ((Errors)ret).ToString());
|
||||||
|
return bitrate;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
throw new ObjectDisposedException("OpusEncoder");
|
||||||
|
var ret = API.opus_encoder_ctl(_encoder, Ctl.SetBitrateRequest, value);
|
||||||
|
if (ret < 0)
|
||||||
|
throw new Exception("Encoder error - " + ((Errors)ret).ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether Forward Error Correction is enabled.
|
||||||
|
/// </summary>
|
||||||
|
public bool ForwardErrorCorrection
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_encoder == IntPtr.Zero)
|
||||||
|
throw new ObjectDisposedException("OpusEncoder");
|
||||||
|
|
||||||
|
int fec;
|
||||||
|
int ret = API.opus_encoder_ctl(_encoder, Ctl.GetInbandFECRequest, out fec);
|
||||||
|
if (ret < 0)
|
||||||
|
throw new Exception("Encoder error - " + ((Errors)ret).ToString());
|
||||||
|
|
||||||
|
return fec > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_encoder == IntPtr.Zero)
|
||||||
|
throw new ObjectDisposedException("OpusEncoder");
|
||||||
|
|
||||||
|
var ret = API.opus_encoder_ctl(_encoder, Ctl.SetInbandFECRequest, value ? 1 : 0);
|
||||||
|
if (ret < 0)
|
||||||
|
throw new Exception("Encoder error - " + ((Errors)ret).ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~OpusEncoder()
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool disposed;
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
|
||||||
|
if (_encoder != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
API.opus_encoder_destroy(_encoder);
|
||||||
|
_encoder = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user