Click here to Skip to main content
15,892,674 members
Articles / Desktop Programming / WPF

The WPF Podcatcher Series - Part 2 (Structural Skinning)

Rate me:
Please Sign up or sign in to vote.
4.97/5 (58 votes)
5 Mar 2008CPOL16 min read 253.4K   7.2K   166  
The second article in a series devoted to a WPF application that plays streaming audio podcasts off the Internet. This article discusses the idea and implementation of look-less applications.
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Media;
using System.Windows.Threading;
using Podder.Model;

namespace Podder
{
    /// <summary>
    /// A MediaPlayer subclass specialized to play podcast episodes.
    /// This class is chock full of workarounds for MediaPlayer bugs and shortcomings.
    /// </summary>
    internal class EpisodePlayer : MediaPlayer
    {
        #region Data

        // This is part of a workaround for a MediaPlayer quirk.
        const int ATTEMPTS_BEFORE_RESTART = 5;

        bool _downloadHasBegun;
        readonly DispatcherTimer _downloadTimer;
        int _numberOfCompletedDownloadProgressValues;
        readonly DispatcherTimer _positionTimer;
        readonly EpisodePlayerSettings _settings;
        bool _wasPlayingBeforeLostNetworkConnection;

        #endregion // Data

        #region Constructor

        public EpisodePlayer()
        {
            _downloadTimer = new DispatcherTimer(DispatcherPriority.Normal);
            _downloadTimer.Interval = TimeSpan.FromSeconds(1);
            _downloadTimer.Tick += this.OnEpisodeDownloadTimerTick;

            _positionTimer = new DispatcherTimer(DispatcherPriority.Send);
            _positionTimer.Interval = TimeSpan.FromSeconds(1);
            _positionTimer.Tick += this.OnPositionTimerTick;

            _settings = PodderDataSource.Instance.EpisodePlayerSettings;
            _settings.PropertyChanged += this.OnEpisodePlayerSettingsPropertyChanged;

            NetworkConnection networkConn = PodderDataSource.Instance.NetworkConnection;
            networkConn.PropertyChanged += delegate
            {
                if (!networkConn.IsAvailable && _settings.IsPlaying)
                {
                    this.PauseEpisode();
                    _wasPlayingBeforeLostNetworkConnection = true;
                }
                else if (networkConn.IsAvailable && _wasPlayingBeforeLostNetworkConnection)
                {
                    this.PlayEpisode();
                    _wasPlayingBeforeLostNetworkConnection = false;
                }
            };
        }

        #endregion // Constructor

        #region Public Interface

        /// <summary>
        /// Returns true if the specified position can be seeked to.
        /// </summary>
        public bool CanSeekTo(TimeSpan position)
        {
            // Don't let the user seek to the end, because then the episode
            // immediately ends, which can be confusing. Also sometimes the
            // MediaPlayer does not raise the MediaEnded event if you seek
            // too close to the end of the episode, so we disallow that.
            if (base.NaturalDuration.HasTimeSpan)
            {
                TimeSpan duration = base.NaturalDuration.TimeSpan;
                duration = duration.Subtract(TimeSpan.FromSeconds(1));
                if (position < duration)
                    return true;
            }

            return false;
        }

        /// <summary>
        /// Raised when EpisodePlayer forces the active episode to stop playing (this is a workaround).
        /// </summary>
        public event EventHandler MediaForcedToEnd;

        public void PauseEpisode()
        {
            base.Pause();
            _settings.SetIsPaused(true);
        }

        public void PlayEpisode()
        {
            base.Play();
            _settings.SetIsPaused(false);
            this.AdjustVolume();
        }

        #endregion // Public Interface

        #region Handle EpisodePlayerSettings Property Changed

        void OnEpisodePlayerSettingsPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "ActiveEpisode")
            {
                this.OnActiveEpisodeChanged();
            }
            else if (e.PropertyName == "Volume" || e.PropertyName == "IsMuted")
            {
                this.AdjustVolume();
            }
        }

        #endregion // Handle EpisodePlayerSettings Property Changed

        #region Process ActiveEpisode Changed

        void OnActiveEpisodeChanged()
        {
            Episode activeEpisode = _settings.ActiveEpisode;

            if (activeEpisode != null)
            {
                base.Open(new Uri(activeEpisode.Url));
                this.PlayEpisode();

                bool isLocalFile = base.Source.IsLoopback;
                if (!isLocalFile)
                    _downloadTimer.Start();

                _positionTimer.Start();
            }
            else
            {
                if (_downloadTimer.IsEnabled)
                    _downloadTimer.Stop();

                if (_positionTimer.IsEnabled)
                    _positionTimer.Stop();

                base.Stop();
                base.Close();

                _settings.PropertyChanged -= this.OnEpisodePlayerSettingsPropertyChanged;
            }
        }

        #endregion // Process ActiveEpisode Changed

        #region MP3 Downloading Workaround

        /// <summary>
        /// Invoked periodically while the active episode MP3 file is being downloaded.
        /// </summary>
        void OnEpisodeDownloadTimerTick(object sender, EventArgs e)
        {
            Debug.WriteLine("Download Progress: " + base.DownloadProgress);

            bool haveNaturalDuration = !_settings.HasActiveEpisodeLength && base.NaturalDuration.HasTimeSpan;
            if (haveNaturalDuration)
                this.SetActiveEpisodeLength();

            // This is a workaround for strange MediaPlayer behavior.
            if (this.ShouldIgnoreCurrentDownloadProgress)
                return;

            // If we made it to five tries and the DownloadProgress is still 1.0, then restarting
            // the MediaPlayer is the only way to force it to start cooperating.
            bool needToRestart =
                base.DownloadProgress == 1.0 &&
                _numberOfCompletedDownloadProgressValues == ATTEMPTS_BEFORE_RESTART;

            if (needToRestart)
            {
                this.Restart();
            }
            else
            {
                // If we reach this point, the download has started and we are good to go.
                _downloadHasBegun = true;

                if (!_settings.IsActiveEpisodeDownloaded)
                    this.UpdateActiveEpisodeDownloadPercent();

                // If we have everything we need, stop the download timer.
                if (_settings.IsActiveEpisodeDownloaded && _settings.HasActiveEpisodeLength)
                    _downloadTimer.Stop();
            }
        }

        /// <summary>
        /// Tries to kickstart the MediaPlayer so that it will correctly download the MP3 file.
        /// </summary>
        void Restart()
        {
            Debug.WriteLine("Restarting the EpisodePlayer...");

            _downloadHasBegun = false;
            _numberOfCompletedDownloadProgressValues = 0;

            Uri src = this.Source;
            base.Close();
            base.Open(src);
            this.PlayEpisode();
        }

        /// <summary>
        /// For some strange reason the MediaPlayer's DownloadProgress sometimes returns 1.0 for a little
        /// while after you first assign it a new episode to play.  To workaround this behavior
        /// I ignore any value of 1.0 until it has provided a smaller value first.  However, sometimes
        /// an episode always reports DownloadProgress as 1.0.  In that situation we need to 
        /// restart the MediaPlayer, which usually fixes the problem.
        /// </summary>
        bool ShouldIgnoreCurrentDownloadProgress
        {
            get
            {
                bool mediaPlayerIsBeingStupid =
                    !_downloadHasBegun &&
                    base.DownloadProgress == 1.0 &&
                    ++_numberOfCompletedDownloadProgressValues < ATTEMPTS_BEFORE_RESTART;

                return mediaPlayerIsBeingStupid;
            }
        }

        void SetActiveEpisodeLength()
        {
            // Let the UI know the episodes total length.
            _settings.ActiveEpisodeLength = (int)base.NaturalDuration.TimeSpan.TotalMilliseconds + 1;
            Debug.WriteLine("Found natural duration of active episode: " + _settings.ActiveEpisode.Name);
        }

        void UpdateActiveEpisodeDownloadPercent()
        {
            // Let the UI know how much of the episode we have downloaded so far.
            if (base.DownloadProgress == 1.0)
            {
                _settings.ActiveEpisodeDownloadPercent = 100;
                Debug.WriteLine("Finished download of active episode: " + _settings.ActiveEpisode.Name);
            }
            else if (0.0 < base.DownloadProgress)
            {
                _settings.ActiveEpisodeDownloadPercent = (int)(base.DownloadProgress * 100.0);
            }
        }

        #endregion // MP3 Downloading Workaround

        #region Update Volume

        void AdjustVolume()
        {        
            int rawVolume = _settings.Volume;
            double volume = (double)rawVolume / 100.0;
            base.Volume = volume;

            base.IsMuted = _settings.IsMuted;
        }

        #endregion // Update Volume

        #region OnPositionTimerTick

        void OnPositionTimerTick(object sender, EventArgs e)
        {
            if (_settings.IsPaused)
                return;

            // MediaPlayer will sometimes continue to play past the end of the episode.
            // This code prevents it from doing so, by killing the play session.
            if (this.IsPlayingPastEndOfEpisode)
            {
                if (_positionTimer.IsEnabled)
                    _positionTimer.Stop();

                if (_downloadTimer.IsEnabled)
                    _downloadTimer.Stop();

                base.Stop();

                if (this.MediaForcedToEnd != null)
                    this.MediaForcedToEnd(this, EventArgs.Empty);
            }

            // Setting this property allows the UI to know that the playback position changed.
            _settings.ActiveEpisodePosition = (int)base.Position.TotalMilliseconds;

            bool haveNaturalDuration = !_settings.HasActiveEpisodeLength && base.NaturalDuration.HasTimeSpan;
            if (haveNaturalDuration)
            {
                this.SetActiveEpisodeLength();
                this.UpdateActiveEpisodeDownloadPercent();
            }
        }

        bool IsPlayingPastEndOfEpisode
        {
            get
            {
                return
                    _settings.IsPlaying &&
                    base.NaturalDuration.HasTimeSpan &&
                    base.NaturalDuration.TimeSpan < base.Position;
            }
        }

        #endregion // OnPositionTimerTick
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Josh creates software, for iOS and Windows.

He works at Black Pixel as a Senior Developer.

Read his iOS Programming for .NET Developers[^] book to learn how to write iPhone and iPad apps by leveraging your existing .NET skills.

Use his Master WPF[^] app on your iPhone to sharpen your WPF skills on the go.

Check out his Advanced MVVM[^] book.

Visit his WPF blog[^] or stop by his iOS blog[^].

See his website Josh Smith Digital[^].

Comments and Discussions