Created
January 2, 2018 22:38
-
-
Save JKamsker/cee51db04f2b73ece24879684190d14a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using TS3AudioBot.Audio; | |
using TS3AudioBot.Helper; | |
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Globalization; | |
using System.Linq; | |
using System.Text.RegularExpressions; | |
using TS3Client; | |
using TS3Client.Full; | |
using TS3Client.Messages; | |
using TS3AudioBot; | |
using CSCore.Ffmpeg; | |
using CSCore; | |
namespace CosyClient | |
{ | |
public sealed class CosyTeamspeakClient : TeamspeakControl, IPlayerConnection, ITargetManager | |
{ | |
public readonly Ts3FullClient TsFullClient; | |
private const Codec SendCodec = Codec.OpusMusic; | |
private readonly TimeSpan sendCheckInterval = TimeSpan.FromMilliseconds(5); | |
private readonly TimeSpan audioBufferLength = TimeSpan.FromMilliseconds(20); | |
private const uint StallCountInterval = 10; | |
private const uint StallNoErrorCountMax = 5; | |
private static readonly string[] QuitMessages = { | |
"I'm outta here", "You're boring", "Have a nice day", "Bye", "Good night", | |
"Nothing to do here", "Taking a break", "Lorem ipsum dolor sit amet…", | |
"Nothing can hold me back", "It's getting quiet", "Drop the bazzzzzz", | |
"Never gonna give you up", "Never gonna let you down", "Keep rockin' it", | |
"?", "c(ꙩ_Ꙩ)ꜿ", "I'll be back", "Your advertisement could be here", | |
"connection lost", "disconnected", "Requested by API.", | |
"Robert'); DROP TABLE students;--", "It works!! No, wait...", | |
"Notice me, senpai", ":wq" | |
}; | |
private const string PreLinkConf = "-hide_banner -nostats -i \""; | |
private const string PostLinkConf = "\" -ac 2 -ar 48000 -f s16le -acodec pcm_s16le pipe:1"; | |
private string lastLink; | |
private static readonly Regex FindDurationMatch = new Regex(@"^\s*Duration: (\d+):(\d\d):(\d\d).(\d\d)", Util.DefaultRegexConfig); | |
private TimeSpan? parsedSongLength; | |
private readonly object ffmpegLock = new object(); | |
private readonly TimeSpan retryOnDropBeforeEnd = TimeSpan.FromSeconds(10); | |
private bool hasTriedToReconnectAudio; | |
private readonly Ts3FullClientData ts3FullClientData; | |
private float volume = 1; | |
public TargetSendMode SendMode { get; set; } = TargetSendMode.None; | |
public ulong GroupWhisperTargetId { get; set; } | |
public GroupWhisperType GroupWhisperType { get; set; } | |
public GroupWhisperTarget GroupWhisperTarget { get; set; } | |
private TickWorker sendTick; | |
private Process ffmpegProcess; | |
private AudioEncoder encoder; | |
private readonly PreciseAudioTimer audioTimer; | |
private byte[] audioBuffer; | |
private bool isStall; | |
private uint stallCount; | |
private uint stallNoErrorCount; | |
private IdentityData identity; | |
private readonly Dictionary<ulong, bool> channelSubscriptionsSetup; | |
private readonly List<ushort> clientSubscriptionsSetup; | |
private ulong[] channelSubscriptionsCache; | |
private ushort[] clientSubscriptionsCache; | |
private bool subscriptionSetupChanged; | |
private readonly object subscriptionLockObj = new object(); | |
public CosyTeamspeakClient(Ts3FullClientData tfcd) : base(ClientType.Full) | |
{ | |
TsFullClient = (Ts3FullClient)tsBaseClient; | |
ts3FullClientData = tfcd; | |
tfcd.PropertyChanged += Tfcd_PropertyChanged; | |
sendTick = TickPool.RegisterTick(AudioSend, sendCheckInterval, false); | |
encoder = new AudioEncoder(SendCodec) { Bitrate = ts3FullClientData.AudioBitrate * 1000 }; //48100 | |
audioTimer = new PreciseAudioTimer(encoder.SampleRate, encoder.BitsPerSample, encoder.Channels); | |
isStall = false; | |
stallCount = 0; | |
identity = null; | |
Util.Init(ref channelSubscriptionsSetup); | |
Util.Init(ref clientSubscriptionsSetup); | |
subscriptionSetupChanged = true; | |
} | |
public override T GetLowLibrary<T>() | |
{ | |
if (typeof(T) == typeof(Ts3FullClient) && TsFullClient != null) | |
return TsFullClient as T; | |
return base.GetLowLibrary<T>(); | |
} | |
private void Tfcd_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) | |
{ | |
if (e.PropertyName == nameof(Ts3FullClientData.AudioBitrate)) | |
{ | |
var value = (int)typeof(Ts3FullClientData).GetProperty(e.PropertyName).GetValue(sender); | |
if (value <= 0 || value >= 256) | |
return; | |
encoder.Bitrate = value * 1000; | |
} | |
} | |
public override void Connect() | |
{ | |
// get or compute identity | |
if (string.IsNullOrEmpty(ts3FullClientData.Identity)) | |
{ | |
identity = Ts3Crypt.GenerateNewIdentity(); | |
ts3FullClientData.Identity = identity.PrivateKeyString; | |
ts3FullClientData.IdentityOffset = identity.ValidKeyOffset; | |
} | |
else | |
{ | |
identity = Ts3Crypt.LoadIdentity(ts3FullClientData.Identity, ts3FullClientData.IdentityOffset); | |
} | |
// check required security level | |
if (ts3FullClientData.IdentityLevel == "auto") { } | |
else if (int.TryParse(ts3FullClientData.IdentityLevel, out int targetLevel)) | |
{ | |
Log.Write(Log.Level.Info, "Calculating up to required security level: {0}", targetLevel); | |
Ts3Crypt.ImproveSecurity(identity, targetLevel); | |
ts3FullClientData.IdentityOffset = identity.ValidKeyOffset; | |
} | |
else | |
{ | |
Log.Write(Log.Level.Warning, "Invalid value for QueryConnection::IdentityLevel, enter a number or \"auto\"."); | |
} | |
// get or compute password | |
if (!string.IsNullOrEmpty(ts3FullClientData.ServerPassword) | |
&& ts3FullClientData.ServerPasswordAutoHash | |
&& !ts3FullClientData.ServerPasswordIsHashed) | |
{ | |
ts3FullClientData.ServerPassword = Ts3Crypt.HashPassword(ts3FullClientData.ServerPassword); | |
ts3FullClientData.ServerPasswordIsHashed = true; | |
} | |
TsFullClient.QuitMessage = QuitMessages[Util.Random.Next(0, QuitMessages.Length)]; | |
TsFullClient.OnErrorEvent += TsFullClient_OnErrorEvent; | |
ConnectClient(); | |
} | |
private void ConnectClient() | |
{ | |
VersionSign verionSign; | |
if (!string.IsNullOrEmpty(ts3FullClientData.ClientVersion)) | |
{ | |
var splitData = ts3FullClientData.ClientVersion.Split('|').Select(x => x.Trim()).ToArray(); | |
var plattform = (ClientPlattform)Enum.Parse(typeof(ClientPlattform), splitData[1], true); | |
verionSign = new VersionSign(splitData[0], plattform, splitData[2]); | |
} | |
else if (Util.IsLinux) | |
verionSign = VersionSign.VER_LIN_3_0_19_4; | |
else | |
verionSign = VersionSign.VER_WIN_3_0_19_4; | |
TsFullClient.Connect(new ConnectionDataFull | |
{ | |
Username = ts3FullClientData.DefaultNickname, | |
Password = ts3FullClientData.ServerPassword, | |
Address = ts3FullClientData.Address, | |
Identity = identity, | |
IsPasswordHashed = ts3FullClientData.ServerPasswordIsHashed, | |
VersionSign = verionSign, | |
DefaultChannel = ts3FullClientData.DefaultChannel, | |
}); | |
} | |
private void TsFullClient_OnErrorEvent(object sender, CommandError e) | |
{ | |
switch (e.Id) | |
{ | |
case Ts3ErrorCode.whisper_no_targets: | |
stallNoErrorCount = 0; | |
isStall = true; | |
break; | |
case Ts3ErrorCode.client_could_not_validate_identity: | |
if (ts3FullClientData.IdentityLevel == "auto") | |
{ | |
int targetSecLevel = int.Parse(e.ExtraMessage); | |
Log.Write(Log.Level.Info, "Calculating up to required security level: {0}", targetSecLevel); | |
Ts3Crypt.ImproveSecurity(identity, targetSecLevel); | |
ts3FullClientData.IdentityOffset = identity.ValidKeyOffset; | |
ConnectClient(); | |
} | |
else | |
{ | |
Log.Write(Log.Level.Warning, "The server reported that the security level you set is not high enough." + | |
"Increase the value to \"{0}\" or set it to \"auto\" to generate it on demand when connecting.", e.ExtraMessage); | |
} | |
break; | |
default: | |
Log.Write(Log.Level.Debug, e.ErrorFormat()); | |
break; | |
} | |
} | |
public override ClientData GetSelf() | |
{ | |
var data = tsBaseClient.WhoAmI(); | |
var cd = new ClientData | |
{ | |
Uid = identity.ClientUid, | |
ChannelId = data.ChannelId, | |
ClientId = TsFullClient.ClientId, | |
NickName = data.NickName, | |
ClientType = tsBaseClient.ClientType | |
}; | |
try | |
{ | |
var response = tsBaseClient.Send("clientgetdbidfromuid", new TS3Client.Commands.CommandParameter("cluid", identity.ClientUid)).FirstOrDefault(); | |
if (response != null && ulong.TryParse(response["cldbid"], out var dbId)) | |
cd.DatabaseId = dbId; | |
} | |
catch (Ts3CommandException) { } | |
return cd; | |
} | |
private void AudioSend() | |
{ | |
lock (ffmpegLock) | |
{ | |
if (_ffmpegDecoder == null) | |
return; | |
if (audioBuffer == null || audioBuffer.Length < encoder.OptimalPacketSize) | |
audioBuffer = new byte[encoder.OptimalPacketSize]; | |
while (audioTimer.RemainingBufferDuration < audioBufferLength) | |
{ | |
int read = _ffmpegDecoder.Read(audioBuffer, 0, encoder.OptimalPacketSize); | |
if (read == 0) | |
{ | |
// check for premature connection drop | |
if (ffmpegProcess.HasExited && !hasTriedToReconnectAudio) | |
{ | |
var expectedStopLength = GetCurrentSongLength(); | |
if (expectedStopLength != TimeSpan.Zero) | |
{ | |
var actualStopPosition = audioTimer.SongPosition; | |
if (actualStopPosition + retryOnDropBeforeEnd < expectedStopLength) | |
{ | |
Log.Write(Log.Level.Debug, "Connection to song lost, retrying at {0}", actualStopPosition); | |
hasTriedToReconnectAudio = true; | |
Position = actualStopPosition; | |
return; | |
} | |
} | |
} | |
if (ffmpegProcess.HasExited | |
&& audioTimer.RemainingBufferDuration < TimeSpan.Zero | |
&& !encoder.HasPacket) | |
{ | |
AudioStop(); | |
OnSongEnd?.Invoke(this, new EventArgs()); | |
} | |
return; | |
} | |
hasTriedToReconnectAudio = false; | |
audioTimer.PushBytes(read); | |
bool doSend = true; | |
switch (SendMode) | |
{ | |
case TargetSendMode.None: | |
doSend = false; | |
break; | |
case TargetSendMode.Voice: | |
break; | |
case TargetSendMode.Whisper: | |
case TargetSendMode.WhisperGroup: | |
if (isStall) | |
{ | |
if (++stallCount % StallCountInterval == 0) | |
{ | |
stallNoErrorCount++; | |
if (stallNoErrorCount > StallNoErrorCountMax) | |
{ | |
stallCount = 0; | |
isStall = false; | |
} | |
} | |
else | |
{ | |
doSend = false; | |
} | |
} | |
if (SendMode == TargetSendMode.Whisper) | |
doSend &= channelSubscriptionsCache.Length > 0 || clientSubscriptionsCache.Length > 0; | |
break; | |
default: | |
throw new InvalidOperationException(); | |
} | |
// Save cpu when we know there is noone to send to | |
if (!doSend) | |
break; | |
AudioModifier.AdjustVolume(audioBuffer, read, volume); | |
encoder.PushPcmAudio(audioBuffer, read); | |
while (encoder.HasPacket) | |
{ | |
var packet = encoder.GetPacket(); | |
switch (SendMode) | |
{ | |
case TargetSendMode.Voice: | |
TsFullClient.SendAudio(packet.Array, packet.Length, encoder.Codec); | |
break; | |
case TargetSendMode.Whisper: | |
TsFullClient.SendAudioWhisper(packet.Array, packet.Length, encoder.Codec, channelSubscriptionsCache, clientSubscriptionsCache); | |
break; | |
case TargetSendMode.WhisperGroup: | |
TsFullClient.SendAudioGroupWhisper(packet.Array, packet.Length, encoder.Codec, GroupWhisperType, GroupWhisperTarget); | |
break; | |
} | |
encoder.ReturnPacket(packet.Array); | |
} | |
} | |
} | |
} | |
#region IPlayerConnection | |
public event EventHandler OnSongEnd; | |
public void SetGroupWhisper(GroupWhisperType type, GroupWhisperTarget target, ulong targetId = 0) | |
{ | |
GroupWhisperType = type; | |
GroupWhisperTarget = target; | |
GroupWhisperTargetId = targetId; | |
} | |
private IWaveSource _ffmpegDecoder; | |
public R AudioStart(string url) | |
{ | |
//_ffmpegDecoder = new FfmpegDecoder(url).ChangeSampleRate(48100);//48100 //ts3FullClientData.AudioBitrate * 1000 | |
var smp = new FfmpegDecoder(url).ToSampleSource();//.ChangeSampleRate(48100) | |
_ffmpegDecoder = new CSCore.Streams.SampleConverter.SampleToPcm16(smp); | |
lastLink = url; | |
parsedSongLength = null; | |
audioTimer.SongPositionOffset = TimeSpan.Zero; | |
audioTimer.Start(); | |
sendTick.Active = true; | |
return R.OkR; | |
} | |
//StartFfmpegProcess(url); | |
public R AudioStop() | |
{ | |
sendTick.Active = false; | |
audioTimer.Stop(); | |
if (_ffmpegDecoder != null) | |
{ | |
_ffmpegDecoder.Dispose(); | |
_ffmpegDecoder = null; | |
} | |
return R.OkR; | |
} | |
public TimeSpan Length => GetCurrentSongLength(); | |
public TimeSpan Position | |
{ | |
get => _ffmpegDecoder.GetPosition(); | |
set | |
{ | |
if (value < TimeSpan.Zero || value > Length) | |
throw new ArgumentOutOfRangeException(nameof(value)); | |
_ffmpegDecoder.SetPosition(value); | |
/* | |
AudioStop(); | |
StartFfmpegProcess(lastLink, | |
$"-ss {value.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture)}", | |
$"-ss {value.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture)}"); | |
audioTimer.SongPositionOffset = value; | |
*/ | |
} | |
} | |
public int Volume | |
{ | |
get => (int)Math.Round(volume * AudioValues.MaxVolume); | |
set | |
{ | |
if (value < 0 || value > AudioValues.MaxVolume) | |
throw new ArgumentOutOfRangeException(nameof(value)); | |
volume = value / (float)AudioValues.MaxVolume; | |
} | |
} | |
public bool Paused | |
{ | |
get => sendTick.Active; | |
set | |
{ | |
if (sendTick.Active == value) | |
{ | |
sendTick.Active = !value; | |
if (value) | |
{ | |
audioTimer.SongPositionOffset = audioTimer.SongPosition; | |
audioTimer.Stop(); | |
} | |
else | |
audioTimer.Start(); | |
} | |
} | |
} | |
public bool Playing => sendTick.Active; | |
public bool Repeated { get { return false; } set { } } | |
private R StartFfmpegProcess(string url, string extraPreParam = null, string extraPostParam = null) | |
{ | |
try | |
{ | |
lock (ffmpegLock) | |
{ | |
StopFfmpegProcess(); | |
ffmpegProcess = new Process | |
{ | |
StartInfo = new ProcessStartInfo | |
{ | |
FileName = ts3FullClientData.FfmpegPath, | |
Arguments = string.Concat(extraPreParam, " ", PreLinkConf, url, PostLinkConf, " ", extraPostParam), | |
RedirectStandardOutput = true, | |
RedirectStandardInput = true, | |
RedirectStandardError = true, | |
UseShellExecute = false, | |
CreateNoWindow = true, | |
} | |
}; | |
ffmpegProcess.Start(); | |
lastLink = url; | |
parsedSongLength = null; | |
audioTimer.SongPositionOffset = TimeSpan.Zero; | |
audioTimer.Start(); | |
sendTick.Active = true; | |
return R.OkR; | |
} | |
} | |
catch (Exception ex) { return $"Unable to create stream ({ex.Message})"; } | |
} | |
private void StopFfmpegProcess() | |
{ | |
lock (ffmpegLock) | |
{ | |
if (ffmpegProcess == null) | |
return; | |
try | |
{ | |
if (!ffmpegProcess.HasExited) | |
ffmpegProcess.Kill(); | |
else | |
ffmpegProcess.Close(); | |
} | |
catch (InvalidOperationException) { } | |
ffmpegProcess = null; | |
} | |
} | |
private TimeSpan GetCurrentSongLength() | |
{ | |
return _ffmpegDecoder?.GetLength() ?? TimeSpan.MaxValue; | |
//lock (ffmpegLock) | |
//{ | |
// if (ffmpegProcess == null) | |
// return TimeSpan.Zero; | |
// if (parsedSongLength.HasValue) | |
// return parsedSongLength.Value; | |
// Match match = null; | |
// while (ffmpegProcess.StandardError.Peek() > -1) | |
// { | |
// var infoLine = ffmpegProcess.StandardError.ReadLine(); | |
// if (string.IsNullOrEmpty(infoLine)) | |
// continue; | |
// match = FindDurationMatch.Match(infoLine); | |
// if (match.Success) | |
// break; | |
// } | |
// if (match == null || !match.Success) | |
// return TimeSpan.Zero; | |
// int hours = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); | |
// int minutes = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); | |
// int seconds = int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture); | |
// int millisec = int.Parse(match.Groups[4].Value, CultureInfo.InvariantCulture) * 10; | |
// parsedSongLength = new TimeSpan(0, hours, minutes, seconds, millisec); | |
// return parsedSongLength.Value; | |
//} | |
} | |
#endregion IPlayerConnection | |
#region ITargetManager | |
public void WhisperChannelSubscribe(ulong channel, bool temp) | |
{ | |
// TODO move to requested channel | |
// TODO spawn new client | |
lock (subscriptionLockObj) | |
{ | |
if (channelSubscriptionsSetup.TryGetValue(channel, out var subscriptionTemp)) | |
channelSubscriptionsSetup[channel] = !subscriptionTemp || !temp; | |
else | |
{ | |
channelSubscriptionsSetup[channel] = !temp; | |
subscriptionSetupChanged = true; | |
} | |
} | |
} | |
public void WhisperChannelUnsubscribe(ulong channel, bool temp) | |
{ | |
lock (subscriptionLockObj) | |
{ | |
if (!temp) | |
{ | |
subscriptionSetupChanged |= channelSubscriptionsSetup.Remove(channel); | |
} | |
else | |
{ | |
if (channelSubscriptionsSetup.TryGetValue(channel, out bool subscriptionTemp) && subscriptionTemp) | |
{ | |
channelSubscriptionsSetup.Remove(channel); | |
subscriptionSetupChanged = true; | |
} | |
} | |
} | |
} | |
public void WhisperClientSubscribe(ushort userId) | |
{ | |
lock (subscriptionLockObj) | |
{ | |
if (!clientSubscriptionsSetup.Contains(userId)) | |
clientSubscriptionsSetup.Add(userId); | |
subscriptionSetupChanged = true; | |
} | |
} | |
public void WhisperClientUnsubscribe(ushort userId) | |
{ | |
lock (subscriptionLockObj) | |
{ | |
clientSubscriptionsSetup.Remove(userId); | |
subscriptionSetupChanged = true; | |
} | |
} | |
public void ClearTemporary() | |
{ | |
lock (subscriptionLockObj) | |
{ | |
ulong[] removeList = channelSubscriptionsSetup | |
.Where(kvp => kvp.Value) | |
.Select(kvp => kvp.Key) | |
.ToArray(); | |
foreach (var chan in removeList) | |
{ | |
channelSubscriptionsSetup.Remove(chan); | |
subscriptionSetupChanged = true; | |
} | |
} | |
} | |
private void UpdatedSubscriptionCache() | |
{ | |
if (!subscriptionSetupChanged) | |
return; | |
lock (subscriptionLockObj) | |
{ | |
if (!subscriptionSetupChanged) | |
return; | |
channelSubscriptionsCache = channelSubscriptionsSetup.Keys.ToArray(); | |
clientSubscriptionsCache = clientSubscriptionsSetup.ToArray(); | |
subscriptionSetupChanged = false; | |
} | |
} | |
#endregion ITargetManager | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment