Quantcast
Channel: Richard Szalay » Richard Szalay
Viewing all articles
Browse latest Browse all 12

Working around the Windows Phone Bluetooth disconnection issue (updated for GDR2)

$
0
0

Update: This post has been updated to include support for GDR2.

There’s an issue on Windows Phone (7 and 8) whereby disconnecting a Bluetooth headset will cause the current track to play through the loud speaker, even if the track was paused. Not many people talk about the issue because the number of audio apps represent a pretty small percentage of the marketplace and, sadly, those who have worked around it in the past attempt to use it as a marketing advantage rather than share the knowledge on the problem.

For those who don’t care how I solved it, feel free to jump directly to the MIT licensed Gist

Solving the problem requires some trickery, since there are no APIs to notify apps of the connected state of Bluetooth headsets. Even so, identifying the scenario is quite basic, with the AudioPlayerAgent.OnUserAction method being called three times in quick succession: Pause, Seek, Play. The only gotcha here is that each call is made on a different instance of the AudioPlayer, so any state needs to be static.

It turns out identifying the issue is only half the problem. I actually tried several methods of stopping the play from occurring from directly within OnUserAction, including mapping the Play action to a Pause and even ignoring the action completely, but none of my attempts prevented the audio from playing.

The solution, in the end, was to detect the issue in OnUserAction and then correct it in OnPlayerStateChanged by immediately calling Pause. This caused the audio to be heard very briefly, which could be worked around by temporarily setting the volume to 0 when the issue was detected.

The implementation could be improved in the following ways, though whether it’s worth doing I’ll leave to you:

  • The detection sequence could, in theory, be accidentally reproduced by user action and thus detection should be given a time limit. However, since I couldn’t reproduce the sequence through seeking, I decided not to include it.
  • The small, ~50ms, gap where the volume is set to 0 could be worked around by capturing the current position on detection and setting it after the pause in the workaround

Enter GDR2

When Microsoft released GDR2 (General Distribution Release 2, the second major update to WP8), it came with claims that they had fixed the Bluetooth issue. Unfortunately, all it did was break my workaround. The reason is that, now, the user actions aren’t sent; only the PlayStateChanged sequence of Paused, TrackReady. Unfortunately, the sample code that comes with a new “audio agent” (which remains in the Podcaster source, as I’m sure it does many other applications) automatically starts playing on TrackReady. I’ve updated the Gist, as well as the code below, to cater for this alternative sequence of events.

Below is the (MIT licensed) source for the workaround, as well as a bare-bones AudioPlayer implementation that uses it. I recommend getting the code from the GitHub Gist as I might forget this post if I make any improvements.

/*
 Copyright (C) 2013 Richard Szalay

 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 
 to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 
 IN THE SOFTWARE.
*/
namespace RichardSzalay.Phone.BackgroundAudio
{
    using System;
    using System.Collections.Generic;
    using Microsoft.Phone.BackgroundAudio;

    /// <summary>
    /// Works around a known platform issue whereby disconnecting a Bluetooth headset causes the audio to play through the loud speaker, even if the track is currently paused
    /// </summary>
    /// <see cref="http://social.msdn.microsoft.com/Forums/wpapps/en-US/48d29436-6b9e-4a8f-8322-677b08c0443e/disconnect-bluetooth-and-paused-audio-starts-to-play"/>
    /// <see cref="http://social.msdn.microsoft.com/Forums/wpapps/en-US/74c80efe-5af3-473e-baa0-87330c9f6160/disconnecting-from-bluetooth-when-music-is-paused-triggers-a-play-command"/>
    /// <see cref="http://stackoverflow.com/questions/13953787/control-wp7-and-wp8-app-with-bluetooth-headset"/>
    public class PlatformBluetoothIssueWorkaround
    {
        readonly UserAction[] KnownBluetoothIssueUserActionSequence = new[] { UserAction.Pause, UserAction.Seek, UserAction.Play };
        readonly List<UserAction> recordedBluetoothIssueActions = new List<UserAction>();

        readonly PlayState[] KnownBluetoothIssuePlayStateSequence = new[] { PlayState.Paused, PlayState.TrackReady };
        readonly List<PlayState> recordedBluetoothIssueStates = new List<PlayState>();

        double bluetoothSpeakerRecordedVolume = 0;

        /// <summary>
        /// Gets whether a Bluetooth disconnect issue is currently detected
        /// </summary>
        public bool IssueDetected { get; private set; }

        /// <summary>To be called from AudioPlayerAgent.OnUserAction</summary>
        /// <returns>true if a Bluetooth disconnect issue was detected; false otherwise</returns>
        public bool HandleUserAction(BackgroundAudioPlayer player, UserAction action)
        {
            if (ShouldHandlePlayStateSequence)
            {
                return false;
            }

            IssueDetected = (HandleSequenceEntry(KnownBluetoothIssueUserActionSequence, recordedBluetoothIssueActions, action, player));

            if (IssueDetected)
            {
                RecordPlaybackVolume(player);
            }

            return IssueDetected;
        }

        /// <summary>To be called from AudioPlayerAgent.OnPlayStateChanged</summary>
        /// <returns>true if a Bluetooth disconnect issue was recovered from; false otherwise</returns>
        public bool HandlePlayStateChanged(BackgroundAudioPlayer player, PlayState playState)
        {
            if (ShouldHandlePlayStateSequence &&
                HandleSequenceEntry(KnownBluetoothIssuePlayStateSequence, recordedBluetoothIssueStates, playState, player))
            {
                return true;
            }

            if (IssueDetected)
            {
                IssueDetected = false;
                player.Pause();
                RestorePlaybackVolume(player);
                return true;
            }

            return false;
        }

        private bool HandleSequenceEntry<T>(T[] matchSequence, List<T> recordedSequence, T newValue, BackgroundAudioPlayer player)
        {
            if (matchSequence[recordedSequence.Count].Equals(newValue))
            {
                recordedSequence.Add(newValue);

                if (recordedSequence.Count == matchSequence.Length)
                {
                    recordedSequence.Clear();

                    return true;
                }
            }
            else
            {
                recordedSequence.Clear();

                if (matchSequence[0].Equals(newValue))
                {
                    recordedSequence.Add(newValue);
                }
            }

            return false;
        }

        void RecordPlaybackVolume(BackgroundAudioPlayer player)
        {
            bluetoothSpeakerRecordedVolume = player.Volume;
            player.Volume = 0D;
        }

        void RestorePlaybackVolume(BackgroundAudioPlayer player)
        {
            player.Volume = bluetoothSpeakerRecordedVolume;
        }

        // When true, the PlayState sequence will be used. Otherwise, the UserAction sequence will be used.
        private bool ShouldHandlePlayStateSequence
        {
            get
            {
                return IsGeneralDistributionRelease2(Environment.OSVersion.Version);
            }
        }

        static readonly Version MinGdr2Version = new Version(8, 0, 10327, 77);

        /// <summary>Determines whether the supplied OS Version represents WP8 GDR2 or greater</summary>
        public static bool IsGeneralDistributionRelease2(Version osVersion)
        {
            return osVersion >= MinGdr2Version;
        }
    }
}

Example usage:

public class AudioPlayer
{
    // Note "static" !
    static readonly PlatformBluetoothIssueWorkaround bluetoothIssueWorkaround = new PlatformBluetoothIssueWorkaround();

    protected override void OnPlayStateChanged(BackgroundAudioPlayer player, AudioTrack track, PlayState playState)
    {
        if (bluetoothIssueWorkaround.HandlePlayStateChanged(player, playState))
        {
            NotifyComplete();
            return;
        }

        // Process as normal
    }

    protected override void OnUserAction(BackgroundAudioPlayer player, AudioTrack track, UserAction action, object param)
    {
        bluetoothIssueWorkaround.HandleUserAction(player, action);

        // Process as normal
    }
}


Viewing all articles
Browse latest Browse all 12

Latest Images

Trending Articles





Latest Images