sipsorcery-org / sipsorcery

A WebRTC, SIP and VoIP library for C# and .NET. Designed for real-time communications apps.
https://sipsorcery-org.github.io/sipsorcery
Other
1.45k stars 438 forks source link

.NET Framework timing problem #471

Open bpg-software opened 3 years ago

bpg-software commented 3 years ago

In order to avoid .NET Framework timing problem please evaluate if you find useful to replace (or to create a new object) all timers of AudioExtraSource with MicroTimer or others alternative. The problem is that using Timer the audio stream is played fragmented because Windows play every packets every 30ms and not every 20ms. Thank you

sipsorcery commented 3 years ago

See #404 for an example of a potential solution.

skidwore commented 2 years ago

In researching the issue of System.Threading.Timer not responding to timerBeginPeriod or NtSetTimerResolution, I found that Thread.Sleep is not affected and responds to timerBeginPeriod and NtSetTimerResolution. Since Thread.Sleep does not tie up a complete core like using a SpinWait or something, I wrote a basic timer class to mimic System.Threading.Timer for use in AudioExtrasSource.cs.

This is not perfect, but it does work to make the library function when 20 ms RTP packets are required.

Also, I am not a professional coder. I'm sure this example below could use PLENTY of corrections and upgrades. So be nice if you don't like what I did and consider the cost of this information - free.

namespace ReplacementTimer
{
    public class AlternateTimer : IDisposable
    {
        private TimerCallback Callback;
        private int DueTime = Timeout.Infinite;
        private int Period = Timeout.Infinite;
        private Thread worker = null;
        private CancellationTokenSource cancelToken;
        private bool _disposed = false;
        private string workerName = Guid.NewGuid().ToString();

        public AlternateTimer(TimerCallback callback, object state, int dueTime, int period)
        {
            //State is discarded
            Callback = callback;
            DueTime = dueTime;
            Period = period;

            if (DueTime != Timeout.Infinite || Period != Timeout.Infinite)
            {
                Start();
            }
        }

        private void Start()
        {
            if (worker != null) 
            { 
                Stop();
                worker = null;
            }
            worker = new Thread(new ParameterizedThreadStart(doWork));
            worker.Name = workerName;
            cancelToken = new CancellationTokenSource();
            worker.Start(new TimerObjects(Callback, DueTime, Period));
        }

        private void Stop()
        {
            if (worker != null && worker.IsAlive)
            {
                cancelToken.Cancel();
            }
        }

        public void Change(int dueTime, int period)
        {
            if (dueTime != Timeout.Infinite || period != Timeout.Infinite)
            {
                Stop();
                DueTime = dueTime;
                Period = period;
                Start();
            }
            else
            {
                Stop();
            }
        }

        public void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }

            if (disposing)
            {
                Stop();

                //If dispose is not being called from its own worker thread then really dispose items
                //Otherwise setting objects still in use to null and joining self are problematic
                if (!(Thread.CurrentThread.Name == workerName))
                {
                    if (worker != null && worker.IsAlive)
                    {
                        cancelToken.Cancel();
                        worker.Join();
                    }
                    Callback = null;
                    worker = null;
                    cancelToken = null;
                }
            }

            _disposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
        }

        private void doWork(object obj)
        {
            WinApi.TimeBeginPeriod(1);

            try
            {
                TimerObjects to = (TimerObjects)obj;

                if (to.DueTime == Timeout.Infinite) { return; }

                Thread.Sleep(to.DueTime);
                to.CallBack.Invoke(new object());
                if (to.Period != Timeout.Infinite)
                {
                    int sleepTime = to.Period;
                    //This code takes a little time to run and causes RTP packets to go out a little late so this is a small
                    //attempt to compensate by taking 1 ms off of the desired time if it is greater than two to begin with
                    if (to.Period > 1) { sleepTime--; }
                    while (!cancelToken.IsCancellationRequested)
                    {
                        Thread.Sleep(sleepTime);
                        to.CallBack.Invoke(new object());
                    }
                }
            }
            catch (ThreadAbortException) { }
            finally
            {
                WinApi.TimeEndPeriod(1);
            }
        }

        private class TimerObjects
        {
            internal TimerCallback CallBack;
            internal int DueTime;
            internal int Period;

            internal TimerObjects(TimerCallback cb, int dueTime, int period)
            {
                CallBack = cb;
                DueTime = dueTime;
                Period = period;
            }
        }

        static class WinApi
        {
            /// <summary>TimeBeginPeriod(). See the Windows API documentation for details.</summary>

            [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Interoperability", "CA1401:PInvokesShouldNotBeVisible"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2118:ReviewSuppressUnmanagedCodeSecurityUsage")]
            [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", SetLastError = true)]

            public static extern uint TimeBeginPeriod(uint uMilliseconds);

            /// <summary>TimeEndPeriod(). See the Windows API documentation for details.</summary>

            [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Interoperability", "CA1401:PInvokesShouldNotBeVisible"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2118:ReviewSuppressUnmanagedCodeSecurityUsage")]
            [DllImport("winmm.dll", EntryPoint = "timeEndPeriod", SetLastError = true)]

            public static extern uint TimeEndPeriod(uint uMilliseconds);
        }
    }
}
sipsorcery commented 2 years ago

Also, I am not a professional coder. I'm sure this example below could use PLENTY of corrections and upgrades. So be nice if you don't like what I did and consider the cost of this information - free.

Your example doesn't look too bad to me. It should be a good basis for anyone that wants to attempt to improve the timing accuracy with the .NET Framework on Windows.

It's not suitable for inclusion in the main sipsorcery library due to the Windows specific PInvoke calls. I guess it'd be feasible to create a new package for a .NET Framework and Windows specific audio functions. I don't know how much interest they'd be though now that .NET5 and .NET6 are where all the action is happening.

codymullins commented 2 years ago

Wonder if the new PeriodicTimer is a good candidate here?

https://docs.microsoft.com/en-us/dotnet/api/system.threading.periodictimer?view=net-6.0

Krockema commented 1 year ago

Hi Folks,

In short, no. I tested PeriodicTimer, it has the same issue sub 50 ms, with 50 seems stable. The micro timer, https://github.com/sipsorcery-org/sipsorcery/issues/404#issuecomment-768522959 or (nugte) PrecisionTimer.net seems a better option.

BUUUUT:

The order of Initialisation of AudioExtraSource, MediaEndpoint, and VoIPMediaSession had a major impact on the outgoing voice Quality. I got it running WITHOUT MicroTimer and default values. By creating a VoIPMediaSessionHelper. Minimum working example:

    private VoIPMediaSession? _voipMediaSession { get; set; }
    private MediaEndPoints _mediaEndPoints { get; set; }
    private IAudioSource _audioExtrasSource { get; set; }

    public VoIPMediaSessionHelper() {
        // This works as clean Source
        _audioExtrasSource = new AudioExtrasSource(new AudioEncoder(), new AudioSourceOptions
        {
            MusicInputSamplingRate = AudioSamplingRatesEnum.Rate16KHz,
            AudioSource = AudioSourcesEnum.None
        });

        _mediaEndPoints = new MediaEndPoints { AudioSource = _audioExtrasSource };
    }

Initialize the VoIPMediaSessionHelper and attach the IncommingCallAction to the user client.

_userAgent.OnIncomingCall += new Action<SIPUserAgent,SIPRequest>(voipMediaSessionHelper.GetOnIncommingCallAction); And:

 public async void GetOnIncommingCallAction(SIPUserAgent sipUserAgent, SIPRequest sipRequest) {
     _logger.Info("GetOnIncommingCallAction invoked");

     _voipMediaSession = new VoIPMediaSession(_mediaEndPoints);

     var acceptedCall = sipUserAgent.AcceptCall(sipRequest);
     await sipUserAgent.Answer(acceptedCall, _voipMediaSession);

      var stream = new FileStream(<path to file>, FileMode.Open, FileAccess.Read);
      await _voipMediaSession.AudioExtrasSource.SendAudioFromStream(stream, AudioSamplingRatesEnum.Rate16KHz);
      await _voipMediaSession.AudioExtrasSource.StartAudio();
 }

Through my struggle I noticed the AudioExtrasSource handles the audio file given through AudioSourceOptions diffrently then given by _SendAudioFromSream(...) , even though they both basically read from Filestream in the end. (Separate streams, separate timer in the AudioExtraSource) and I am not certain why. I guess if you have no AudioSourceOptions or Music, it initializes music and the streams / timer infer each other somehow.

It is working for me now. Good luck.

StephenHodgson commented 2 weeks ago

Can confirm @Krockema's solution above seems to resolve all of the poor outbound audio quality issues.

var audioExtrasSource = new AudioExtrasSource(new AudioEncoder(), new AudioSourceOptions
{
    MusicInputSamplingRate = AudioSamplingRatesEnum.Rate16KHz,
    AudioSource = AudioSourcesEnum.None
});
var mediaEndPoints = new MediaEndPoints { AudioSource = audioExtrasSource };
var voipMediaSession = new VoIPMediaSession(mediaEndPoints);