using System; using System.Collections; using System.IO; using System.Net; using System.Text; using System.Threading; using Crosstales.Radio.Model; using Crosstales.Radio.Model.Enum; using Crosstales.Radio.Util; using NAudio.Wave; using NLayer; using NVorbis; using UnityEngine; namespace Crosstales.Radio { [ExecuteInEditMode] [RequireComponent(typeof(AudioSource))] [HelpURLAttribute("https://www.crosstales.com/media/data/assets/radio/api/class_crosstales_1_1_radio_1_1_radio_player.html")] public class RadioPlayer : BasePlayer { [Header("Radio Station")] [Tooltip("Radio station for this RadioPlayer.")] public RadioStation Station; [Tooltip("Play the radio on start on/off (default: false).")] [Header("Behaviour Settings")] public bool PlayOnStart; [Tooltip("Starts and stops the RadioPlayer depending on the focus and running state (default: false).")] public bool HandleFocus; [Tooltip("Size of cache stream in KB (default: 512).")] [Header("General Settings")] public int CacheStreamSize = 512; [Tooltip("Enable or disable legacy mode. This disables all record informations, but is more stable (default: false).")] public bool LegacyMode; [Tooltip("Capture the encoded PCM-stream from this RadioPlayer (default: false).")] public bool CaptureDataStream; private bool error; private string errorMessage; private MpegFile nLayerReader; private Mp3FileReader nAudioReader; private VorbisReader nVorbisReader; private int oggCacheCleanFrameCount; private bool stopped = true; private bool bufferAvailable; private bool playback; private bool restarted; private float maxPlayTime; private Thread worker; private RecordInfo recordInfo = new RecordInfo(); private RecordInfo nextRecordInfo = new RecordInfo(); private RecordInfo lastNextRecordInfo; private float nextRecordDelay; private Stream ms; private static bool loggedUnsupportedPlatform; private static int playCounter; private static bool ignoreExcludeCodec; private bool wasRunning; public override RadioStation RadioStation { get { return Station; } set { Station = value; } } public override bool isLegacyMode { get { return LegacyMode; } set { LegacyMode = value; } } public override bool isCaptureDataStream { get { return CaptureDataStream; } set { CaptureDataStream = value; } } public override AudioSource Source { get; protected set; } public override AudioCodec Codec { get; protected set; } public override float PlayTime { get; protected set; } public override float BufferProgress { get; protected set; } public override bool isPlayback { get { return playback; } } public override bool isAudioPlaying { get { return playback && !isBuffering; } } public override bool isBuffering { get { return !bufferAvailable; } } public override float RecordPlayTime { get; protected set; } public override RecordInfo RecordInfo { get { return recordInfo; } } public override RecordInfo NextRecordInfo { get { return nextRecordInfo; } } public override float NextRecordDelay { get { return nextRecordDelay; } } public override long CurrentBufferSize { get { if (ms != null) { return ms.Length - ms.Position; } return 0L; } } public override long CurrentDownloadSpeed { get { if (ms != null && PlayTime > 0f) { return (long)((float)ms.Length / PlayTime); } return 0L; } } public override MemoryCacheStream DataStream { get; protected set; } public override int Channels { get; protected set; } public override int SampleRate { get; protected set; } public static bool isPlaying { get { return playCounter > 0; } } public void Awake() { Channels = 2; SampleRate = 44100; oggCacheCleanFrameCount = UnityEngine.Random.Range(Constants.OGG_CLEAN_INTERVAL_MIN, Constants.OGG_CLEAN_INTERVAL_MAX); Source = GetComponent(); Source.playOnAwake = false; Source.Stop(); } public void Start() { if (PlayOnStart && !Helper.isEditorMode) { Play(); } } public void Update() { if (isAudioPlaying && !restarted) { if (lastNextRecordInfo == null || !lastNextRecordInfo.Equals(NextRecordInfo)) { lastNextRecordInfo = NextRecordInfo; onNextRecordChange(Station, NextRecordInfo, nextRecordDelay); } float num = Time.unscaledDeltaTime * Source.pitch; Context.TotalPlayTime += num; Station.TotalPlayTime += num; PlayTime += num; RecordPlayTime += num; nextRecordDelay -= num; onAudioPlayTimeUpdate(Station, PlayTime); onRecordPlayTimeUpdate(Station, RecordInfo, RecordPlayTime); onNextRecordDelayUpdate(Station, NextRecordInfo, nextRecordDelay); if (PlayTime > maxPlayTime) { restarted = true; Restart(); } } } public void OnDisable() { Stop(); } public void OnValidate() { if (Station != null) { if (Station.Bitrate <= 0) { Station.Bitrate = Config.DEFAULT_BITRATE; } else { Station.Bitrate = Helper.NearestBitrate(Station.Bitrate, Station.Format); } if (Station.ChunkSize <= 0) { Station.ChunkSize = Config.DEFAULT_CHUNKSIZE; } else if (Station.ChunkSize > Config.MAX_CACHESTREAMSIZE) { Station.ChunkSize = Config.MAX_CACHESTREAMSIZE; } if (Station.BufferSize <= 0) { Station.BufferSize = Config.DEFAULT_BUFFERSIZE; } else { if (Station.Format == AudioFormat.MP3) { if (Station.BufferSize < Config.DEFAULT_BUFFERSIZE / 4) { Station.BufferSize = Config.DEFAULT_BUFFERSIZE / 4; } } else if (Station.Format == AudioFormat.OGG && Station.BufferSize < 64) { Station.BufferSize = 64; } if (Station.BufferSize < Station.ChunkSize) { Station.BufferSize = Station.ChunkSize; } else if (Station.BufferSize > Config.MAX_CACHESTREAMSIZE) { Station.BufferSize = Config.MAX_CACHESTREAMSIZE; } } } if (CacheStreamSize <= 0) { CacheStreamSize = Config.DEFAULT_CACHESTREAMSIZE; } else if (Station != null && CacheStreamSize <= Station.BufferSize) { CacheStreamSize = Station.BufferSize; } else if (CacheStreamSize > Config.MAX_CACHESTREAMSIZE) { CacheStreamSize = Config.MAX_CACHESTREAMSIZE; } } public void OnApplicationFocus(bool hasFocus) { if (!Application.runInBackground && HandleFocus) { if (!hasFocus) { wasRunning = playback; Stop(); } else if (wasRunning) { Play(); } } } public override void Play() { if (Station == null || Station.Name == null) { Debug.LogError("Station == null || Station.Name == null; check Radios_user.txt"); } else if (Helper.isSupportedPlatform) { if (stopped) { if (Helper.isInternetAvailable) { if (Helper.isSane(ref Station)) { Codec = Helper.AudioCodecForAudioFormat(Station.Format); if (Codec == AudioCodec.None) { errorMessage = string.Concat(Station, Environment.NewLine, "Audio format not supported - can't play station: ", Station.Format); Debug.LogError(errorMessage); onErrorInfo(Station, errorMessage); } else if (!ignoreExcludeCodec && Station.ExcludedCodec == Codec) { errorMessage = string.Concat(Station, Environment.NewLine, "Excluded codec matched - can't play station: ", Codec); Debug.LogError(errorMessage); onErrorInfo(Station, errorMessage); } else { StartCoroutine(playAudioFromUrl()); } } else { errorMessage = string.Concat(Station, Environment.NewLine, "Could not start playback. Please verify the station settings."); Debug.LogError(errorMessage); onErrorInfo(Station, errorMessage); } } else { errorMessage = "No internet connection available! Can't play (stream) any stations!"; Debug.LogError(errorMessage); onErrorInfo(Station, errorMessage); } } else { errorMessage = string.Concat(Station, Environment.NewLine, "Station is already playing!"); Debug.LogWarning(errorMessage); onErrorInfo(Station, errorMessage); } } else { logUnsupportedPlatform(); } } public override void Stop() { playback = false; if (Source != null) { Source.Stop(); Source.clip = null; } stopped = true; } public override void Silence() { Source.volume = 0f; } public override void Restart(float invokeDelay = 0.4f) { Stop(); Invoke("Play", invokeDelay); } public string ToShortString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Station='"); stringBuilder.Append(Station); stringBuilder.Append(Constants.TEXT_TOSTRING_DELIMITER); stringBuilder.Append("PlayOnStart='"); stringBuilder.Append(PlayOnStart); stringBuilder.Append(Constants.TEXT_TOSTRING_DELIMITER); stringBuilder.Append("CacheStreamSize='"); stringBuilder.Append(CacheStreamSize); stringBuilder.Append(Constants.TEXT_TOSTRING_DELIMITER_END); return stringBuilder.ToString(); } public void Load() { Debug.LogWarning("Not implemented!"); } public void Save() { Debug.LogWarning("Not implemented!"); } private IEnumerator playAudioFromUrl() { onPlaybackStart(Station); playback = false; restarted = false; error = false; errorMessage = string.Empty; PlayTime = 0f; RecordPlayTime = 0f; BufferProgress = 0f; bufferAvailable = false; float _bufferCurrentProgress = 0f; recordInfo = new RecordInfo(); nextRecordInfo = new RecordInfo(); nextRecordDelay = 0f; onBufferingStart(Station); onBufferingProgressUpdate(Station, BufferProgress); using (Stream stream = (ms = new MemoryCacheStream(CacheStreamSize * 1024, Config.MAX_CACHESTREAMSIZE * 1024))) { if (LegacyMode) { worker = new Thread((ThreadStart)delegate { readStreamLegacy(ref Station, ref playback, ref ms, ref error, ref errorMessage); }); } else { worker = new Thread((ThreadStart)delegate { readStream(ref Station, ref playback, ref ms, ref error, ref errorMessage, ref nextRecordInfo, ref nextRecordDelay); }); } worker.Start(); do { yield return null; } while (!playback && !stopped && !error); int bufferSize = Station.BufferSize * 1024 + Station.ChunkSize * 1024; do { BufferProgress = (float)ms.Length / (float)bufferSize; if (BufferProgress != _bufferCurrentProgress) { onBufferingProgressUpdate(Station, BufferProgress); _bufferCurrentProgress = BufferProgress; } yield return null; } while (playback && !stopped && ms.Length < bufferSize); BufferProgress = 1f; onBufferingProgressUpdate(Station, BufferProgress); bufferAvailable = true; onBufferingEnd(Station); if (playback && !stopped) { bool _success = false; try { if (Codec == AudioCodec.MP3_NLayer) { nLayerReader = new MpegFile(ms); SampleRate = nLayerReader.SampleRate; Channels = nLayerReader.Channels; } else if (Codec == AudioCodec.MP3_NAudio) { nAudioReader = new Mp3FileReader(ms); SampleRate = nAudioReader.WaveFormat.SampleRate; Channels = nAudioReader.WaveFormat.Channels; } else if (Codec == AudioCodec.OGG_NVorbis) { nVorbisReader = new VorbisReader(ms, false); SampleRate = nVorbisReader.SampleRate; Channels = nVorbisReader.Channels; } _success = true; } catch (Exception ex) { Debug.LogError(string.Concat(Station, Environment.NewLine, "Could not read data from url!", Environment.NewLine, ex)); } if (!_success) { error = true; errorMessage = string.Concat(Station, Environment.NewLine, "Could not play the stream -> Please try another station!"); Debug.LogError(errorMessage); playback = false; } else { if (Codec == AudioCodec.OGG_NVorbis) { ms.Position = 0L; } maxPlayTime = int.MaxValue / SampleRate - 240; DataStream = new MemoryCacheStream(131072, 524288); AudioClip clip = AudioClip.Create(Station.Name, int.MaxValue, Channels, SampleRate, true, readPCMData); Source.clip = clip; Source.Play(); onAudioStart(Station); } do { yield return null; if (Codec == AudioCodec.OGG_NVorbis && Time.frameCount % oggCacheCleanFrameCount == 0) { if (Constants.DEV_DEBUG) { Debug.Log("Clean cache: " + oggCacheCleanFrameCount + " - " + PlayTime); } Mdct.ClearSetupCache(); } } while (playback && !stopped); Source.Stop(); Source.clip = null; if (_success) { onAudioEnd(Station); } if (Codec == AudioCodec.MP3_NLayer) { if (nLayerReader != null) { nLayerReader.Dispose(); nLayerReader = null; } } else if (Codec == AudioCodec.MP3_NAudio) { if (nAudioReader != null) { nAudioReader.Dispose(); nAudioReader = null; } } else if (Codec == AudioCodec.OGG_NVorbis && nVorbisReader != null) { nVorbisReader.Dispose(); nVorbisReader = null; Mdct.ClearSetupCache(); } } } if (DataStream != null) { DataStream.Dispose(); } if (error) { onErrorInfo(Station, errorMessage); } onPlaybackEnd(Station); } private void readPCMData(float[] data) { if (playback && !stopped && bufferAvailable) { if (Codec == AudioCodec.MP3_NLayer) { if (nLayerReader != null) { try { if (nLayerReader.ReadSamples(data, 0, data.Length) <= 0) { logNoMoreData(); } } catch (Exception ex) { logDataError(ex); } } } else if (Codec == AudioCodec.MP3_NAudio) { if (nAudioReader != null) { byte[] array = new byte[data.Length * 2]; try { int count; if ((count = nAudioReader.Read(array, 0, array.Length)) > 0) { Buffer.BlockCopy(Helper.ConvertByteArrayToFloatArray(array, count), 0, data, 0, data.Length * 4); } else { logNoMoreData(); } } catch (Exception ex2) { logDataError(ex2); } } } else if (Codec == AudioCodec.OGG_NVorbis && nVorbisReader != null) { try { if (nVorbisReader.ReadSamples(data, 0, data.Length) <= 0) { logNoMoreData(); } } catch (Exception ex3) { logDataError(ex3); } } } else { Buffer.BlockCopy(new float[data.Length], 0, data, 0, data.Length * 4); } if (CaptureDataStream && DataStream != null) { byte[] array2 = Helper.ConvertFloatArrayToByteArray(data, data.Length); DataStream.Write(array2, 0, array2.Length); } } private void readStream(ref RadioStation _station, ref bool _playback, ref Stream _ms, ref bool _error, ref string _errorMessage, ref RecordInfo _nextRecordInfo, ref float _nextRecordDelay) { if (_station.Url.StartsWith(Constants.PREFIX_HTTP) || _station.Url.StartsWith(Constants.PREFIX_HTTPS)) { try { ServicePointManager.ServerCertificateValidationCallback = Helper.RemoteCertificateValidationCallback; using (CTWebClient cTWebClient = new CTWebClient(int.MaxValue)) { HttpWebRequest httpWebRequest = (HttpWebRequest)cTWebClient.CTGetWebRequest(_station.Url); httpWebRequest.Headers.Clear(); httpWebRequest.Headers.Add("GET", "/ HTTP/1.1"); httpWebRequest.Headers.Add("Icy-MetaData", "1"); httpWebRequest.UserAgent = "WinampMPEG/5.09"; using (HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse()) { int num = ((!string.IsNullOrEmpty(httpWebResponse.GetResponseHeader("icy-metaint"))) ? Convert.ToInt32(httpWebResponse.GetResponseHeader("icy-metaint")) : int.MaxValue); if (Constants.DEV_DEBUG) { Debug.LogWarning("metaint: " + num); } using (Stream stream = httpWebResponse.GetResponseStream()) { if (stream != null) { int num2 = 0; byte[] array = new byte[_station.ChunkSize * 1024]; _playback = true; Context.TotalDataRequests++; _station.TotalDataRequests++; int num3 = 0; int num4 = 0; bool flag = true; _nextRecordDelay = 0f; do { int num5; if ((num5 = stream.Read(array, 0, array.Length)) > 0) { Context.TotalDataSize += num5; _station.TotalDataSize += num5; num3 = 0; if (num > 0 && num5 + num4 > num) { int num6 = 0; while (num6 < num5 && _playback) { if (num4 == num) { num4 = 0; _ms.Write(array, num3, num6 - num3); num3 = num6; num2 = Convert.ToInt32(array[num6]) * 16; num6++; num3++; if (num2 > 0) { if (num2 + num3 <= num5) { byte[] array2 = new byte[num2]; Array.Copy(array, num6, array2, 0, num2); _nextRecordInfo = new RecordInfo(Encoding.UTF8.GetString(array2)); if (!flag) { _nextRecordDelay = (float)(_ms.Length - _ms.Position) / (float)(Station.Bitrate * 125); } else { flag = false; } num6 += num2; num3 += num2; if (Constants.DEV_DEBUG) { Debug.LogWarning("RecordInfo read: " + _nextRecordInfo); } } else { if (Constants.DEV_DEBUG) { Debug.LogError("Info-frame outside of the buffer!"); } num6 = num5; num4 = num5 - (num2 + num3); } } } else { num4++; num6++; } } if (num3 < num5) { _ms.Write(array, num3, num5 - num3); } } else { num4 += num5; _ms.Write(array, 0, num5); } } } while (_playback); } return; } } } } catch (Exception ex) { _error = true; _errorMessage = _station.Name + Environment.NewLine + "Could not read url after " + Helper.FormatSecondsToHourMinSec(PlayTime) + "!" + Environment.NewLine + ex; Debug.LogError(_errorMessage); _playback = false; return; } } readStreamLegacy(ref _station, ref _playback, ref _ms, ref _error, ref _errorMessage); } private void readStreamLegacy(ref RadioStation _station, ref bool _playback, ref Stream _ms, ref bool _error, ref string _errorMessage) { try { ServicePointManager.ServerCertificateValidationCallback = Helper.RemoteCertificateValidationCallback; using (CTWebClient cTWebClient = new CTWebClient(int.MaxValue)) { using (WebResponse webResponse = cTWebClient.CTGetWebRequest(_station.Url).GetResponse()) { using (Stream stream = webResponse.GetResponseStream()) { if (stream == null) { return; } byte[] array = new byte[_station.ChunkSize * 1024]; _playback = true; Context.TotalDataRequests++; _station.TotalDataRequests++; do { int num; if ((num = stream.Read(array, 0, array.Length)) > 0) { Context.TotalDataSize += num; _station.TotalDataSize += num; if (_playback) { _ms.Write(array, 0, num); } } } while (_playback); } } } } catch (Exception ex) { _error = true; _errorMessage = _station.Name + Environment.NewLine + "Could not read url after " + Helper.FormatSecondsToHourMinSec(PlayTime) + "!" + Environment.NewLine + ex; Debug.LogError(_errorMessage); _playback = false; } } private void logNoMoreData() { error = true; errorMessage = Station.Name + Environment.NewLine + "No more data to read after " + Helper.FormatSecondsToHourMinSec(PlayTime) + "! Please restart this station or choose another one."; Debug.LogError(errorMessage); playback = false; } private void logDataError(Exception ex) { error = true; errorMessage = Station.Name + Environment.NewLine + "Could not read audio after " + Helper.FormatSecondsToHourMinSec(PlayTime) + "! This is typically a sign of a buffer underun -> Please try to increment the 'ChunkSize' and 'BufferSize':" + Environment.NewLine + ex; Debug.LogError(errorMessage); playback = false; } private void logUnsupportedPlatform() { if (!loggedUnsupportedPlatform) { errorMessage = "'Radio' is not supported on your platform!"; Debug.LogWarning(errorMessage); onErrorInfo(Station, errorMessage); } } private void onPlaybackStart(RadioStation station) { stopped = false; playCounter++; if (Config.DEBUG) { Debug.Log("onPlaybackStart: " + station); } if (_playbackStart != null) { _playbackStart(station); } } private void onPlaybackEnd(RadioStation station) { stopped = true; playCounter--; if (Config.DEBUG) { Debug.Log("onPlaybackEnd: " + station); } if (recordInfo != null) { recordInfo.Duration = RecordPlayTime; recordInfo = new RecordInfo(); } if (_playbackEnd != null) { _playbackEnd(station); } } private void onBufferingStart(RadioStation station) { if (Config.DEBUG) { Debug.Log("onBufferingStart: " + station); } if (_bufferingStart != null) { _bufferingStart(station); } } private void onBufferingEnd(RadioStation station) { if (Config.DEBUG) { Debug.Log("onBufferingEnd: " + station); } if (_bufferingEnd != null) { _bufferingEnd(station); } } private void onBufferingProgressUpdate(RadioStation station, float progress) { if (_bufferingProgressUpdate != null) { _bufferingProgressUpdate(station, progress); } } private void onAudioStart(RadioStation station) { if (Config.DEBUG) { Debug.Log("onAudioStart: " + station); } if (_audioStart != null) { _audioStart(station); } } private void onAudioEnd(RadioStation station) { if (Config.DEBUG) { Debug.Log("onAudioEnd: " + station); } if (_audioEnd != null) { _audioEnd(station); } } private void onAudioPlayTimeUpdate(RadioStation station, float playtime) { if (_audioPlayTimeUpdate != null) { _audioPlayTimeUpdate(station, playtime); } } private void onErrorInfo(RadioStation station, string info) { if (Config.DEBUG) { Debug.Log(string.Concat("onErrorInfo: ", station, " - ", info)); } if (_errorInfo != null) { _errorInfo(station, info); } } private void onRecordChange(RadioStation station, RecordInfo newRecord) { if (!newRecord.Equals(recordInfo)) { if (Config.DEBUG) { Debug.Log(string.Concat("onRecordChange: ", station, " - ", newRecord)); } if (recordInfo != null) { recordInfo.Duration = RecordPlayTime; } recordInfo = newRecord; RecordPlayTime = 0f; if (!string.IsNullOrEmpty(recordInfo.Info)) { Station.PlayedRecords.Add(recordInfo); } if (_recordChange != null) { _recordChange(station, recordInfo); } } } private void onRecordPlayTimeUpdate(RadioStation station, RecordInfo record, float playtime) { if (_recordPlayTimeUpdate != null) { _recordPlayTimeUpdate(station, record, playtime); } } private void onNextRecordChange(RadioStation station, RecordInfo nextRecord, float delay) { if (Config.DEBUG) { Debug.Log(string.Concat("onNextRecordChange: ", station, " - ", nextRecord)); } if (_nextRecordChange != null) { _nextRecordChange(station, nextRecord, delay); } } private void onNextRecordDelayUpdate(RadioStation station, RecordInfo nextRecord, float delay) { if (delay > 0f) { if (_nextRecordDelayUpdate != null) { _nextRecordDelayUpdate(station, nextRecord, delay); } } else { onRecordChange(station, nextRecord); } } public override string ToString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(GetType().Name); stringBuilder.Append(Constants.TEXT_TOSTRING_START); stringBuilder.Append("Station='"); stringBuilder.Append(Station); stringBuilder.Append(Constants.TEXT_TOSTRING_DELIMITER); stringBuilder.Append("PlayOnStart='"); stringBuilder.Append(PlayOnStart); stringBuilder.Append(Constants.TEXT_TOSTRING_DELIMITER); stringBuilder.Append("CacheStreamSize='"); stringBuilder.Append(CacheStreamSize); stringBuilder.Append(Constants.TEXT_TOSTRING_DELIMITER_END); stringBuilder.Append(Constants.TEXT_TOSTRING_END); return stringBuilder.ToString(); } } }