启用从蓝牙连接的远程设备播放音频

本文介绍如何使用 AudioPlaybackConnection 使蓝牙连接的远程设备在本地计算机上播放音频。

从 Windows 10 开始,版本 2004 远程音频源可以将音频流式传输到 Windows 设备,从而实现诸如将电脑配置为类似于蓝牙的扬声器,并允许用户从手机接收音频的方案。 此实现使用操作系统中的蓝牙组件来处理传入的音频数据,并在系统上的系统音频终结点(例如内置电脑扬声器或有线耳机)上播放。 启用基础蓝牙 A2DP 接收器的操作由负责最终用户方案的应用管理,而非由系统进行管理。

AudioPlaybackConnection 类用于启用和禁用来自远程设备的连接,并用于创建连接,以允许开始远程播放音频。

添加用户界面

对于本文中的示例,我们将使用以下简单 XAML UI,该 UI 定义用于显示可用远程设备的 ListView 控件、用于显示连接状态的 TextBlock 以及三个用于启用、禁用和打开连接的按钮。

<Grid x:Name="MainGrid" Loaded="MainGrid_Loaded">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Connection state: "/>
        <TextBlock x:Name="ConnectionState" Grid.Row="0" Text="Disconnected."/>
    </StackPanel>
    <ListView x:Name="DeviceListView" ItemsSource="{x:Bind devices}" Grid.Row="1">
        <ListView.ItemTemplate>
            <DataTemplate x:DataType="enumeration:DeviceInformation">
                <StackPanel Orientation="Horizontal" Margin="6">
                    <SymbolIcon Symbol="Audio" Margin="0,0,12,0"/>
                    <StackPanel>
                        <TextBlock Text="{x:Bind Name}" FontWeight="Bold"/>
                    </StackPanel>
                </StackPanel>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
    <StackPanel Orientation="Vertical" Grid.Row="2">
        <Button x:Name="EnableAudioPlaybackConnectionButton" Content="Enable Audio Playback Connection" Click="EnableAudioPlaybackConnectionButton_Click"/>
        <Button x:Name="ReleaseAudioPlaybackConnectionButton" Content="Release Audio Playback Connection" Click="ReleaseAudioPlaybackConnectionButton_Click"/>
        <Button x:Name="OpenAudioPlaybackConnectionButtonButton" Content="Open Connection" Click="OpenAudioPlaybackConnectionButtonButton_Click" IsEnabled="False"/>
    </StackPanel>
     
</Grid>

使用 DeviceWatcher 监视远程设备

DeviceWatcher 类允许检测连接的设备。 AudioPlaybackConnection.GetDeviceSelector 方法返回一个字符串,该字符串将告知设备观察程序要监视何种类型的设备。 将此字符串传递到 DeviceWatcher 构造函数。

启动设备观察程序时,将为每个已连接的设备引发 DeviceWatcher.Added 事件,设备观察程序运行期间,将为连接的任何设备引发该事件。 如果以前连接的设备断开连接,则会引发 DeviceWatcher.Removed 事件。

调用 DeviceWatcher.Start 以开始监视支持音频播放连接的连接设备。 在此示例中,当加载 UI 中的主 Grid 控件时,将启动设备管理器。 有关使用 DeviceWatcher 的详细信息,请参阅枚举设备

private void MainGrid_Loaded(object sender, RoutedEventArgs e)
{
    audioPlaybackConnections = new Dictionary<string, AudioPlaybackConnection>();

    // Start watching for paired Bluetooth devices. 
    this.deviceWatcher = DeviceInformation.CreateWatcher(AudioPlaybackConnection.GetDeviceSelector());

    // Register event handlers before starting the watcher. 
    this.deviceWatcher.Added += this.DeviceWatcher_Added;
    this.deviceWatcher.Removed += this.DeviceWatcher_Removed;

    this.deviceWatcher.Start();
}

在设备观察程序的 Added 事件中,每个发现的设备均由一个 DeviceInformation 对象表示。 将发现的每个设备添加到绑定到 UI 中的 ListView 控件的可观察集合。

private ObservableCollection<Windows.Devices.Enumeration.DeviceInformation> devices =
    new ObservableCollection<Windows.Devices.Enumeration.DeviceInformation>();

private async void DeviceWatcher_Added(DeviceWatcher sender, DeviceInformation deviceInfo)
{
    // Collections bound to the UI are updated in the UI thread. 
    await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
    {
        this.devices.Add(deviceInfo);
    });
}

启用和释放音频播放连接

在打开与设备的连接之前,必须启用连接。 这会告知系统存在一个新的应用程序,该应用程序想要在电脑上播放远程设备上的音频,但在打开连接之前音频不会开始播放(连接会在后面的步骤中显示)。

在“启用音频播放连接”按钮的单击处理程序中,获取与 ListView 控件中当前所选设备关联的设备 ID。 此示例维护已启用的 AudioPlaybackConnection 对象的字典。 此方法首先检查字典中是否已存在所选设备对应的条目。 接下来,该方法通过调用 TryCreateFromId 并传入选定的设备 ID,尝试为选定的设备创建 AudioPlaybackConnection。

如果成功创建连接,请将新的 AudioPlaybackConnection 对象添加到应用字典,为对象的 StateChanged 事件注册处理程序,并调用 StartAsync 以通知系统已启用新连接。

private Dictionary<String, AudioPlaybackConnection> audioPlaybackConnections;
private async void EnableAudioPlaybackConnectionButton_Click(object sender, RoutedEventArgs e)
{
    if (! (DeviceListView.SelectedItem is null))
    {
        var selectedDeviceId = (DeviceListView.SelectedItem as DeviceInformation).Id;
        if (!this.audioPlaybackConnections.ContainsKey(selectedDeviceId))
        {
            // Create the audio playback connection from the selected device id and add it to the dictionary. 
            // This will result in allowing incoming connections from the remote device. 
            var playbackConnection = AudioPlaybackConnection.TryCreateFromId(selectedDeviceId);

            if (playbackConnection != null)
            {
                // The device has an available audio playback connection. 
                playbackConnection.StateChanged += this.AudioPlaybackConnection_ConnectionStateChanged;
                this.audioPlaybackConnections.Add(selectedDeviceId, playbackConnection);
                await playbackConnection.StartAsync();
                OpenAudioPlaybackConnectionButtonButton.IsEnabled = true;
            }
        }
    }
}

打开音频播放连接

上一步中已创建音频播放连接,但在通过调用 OpenOpenAsync 打开连接之前,不会开始播放声音。 在“打开音频播放连接”按钮上,单击“处理程序”,获取当前所选的设备,并使用该 ID 从应用的连接字典中检索 AudioPlaybackConnection。 等待对 OpenAsync 的调用,并检查返回的 AudioPlaybackConnectionOpenResultStatus 对象的“状态”值,以查看连接是否已成功打开,如果是,则更新“连接状态”文本框。

private async void OpenAudioPlaybackConnectionButtonButton_Click(object sender, RoutedEventArgs e)
{
    var selectedDevice = (DeviceListView.SelectedItem as DeviceInformation).Id;
    AudioPlaybackConnection selectedConnection;

    if (this.audioPlaybackConnections.TryGetValue(selectedDevice, out selectedConnection))
    {
        if ((await selectedConnection.OpenAsync()).Status == AudioPlaybackConnectionOpenResultStatus.Success)
        {
            // Notify that the AudioPlaybackConnection is connected. 
            ConnectionState.Text = "Connected";
        }
        else
        {
            // Notify that the connection attempt did not succeed. 
            ConnectionState.Text = "Disconnected (attempt failed)";
        }
    }
}

监视音频播放连接状态

当连接的状态发生更改时,将引发 AudioPlaybackConnection.ConnectionStateChanged 事件。 在此示例中,此事件的处理程序会更新状态文本框。 请记住,在对 Dispatcher.RunAsync 的调用中更新 UI,以确保在 UI 线程上进行更新。

private async void AudioPlaybackConnection_ConnectionStateChanged(AudioPlaybackConnection sender, object args)
{
    await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        if (sender.State == AudioPlaybackConnectionState.Closed)
        {
            ConnectionState.Text = "Disconnected";
        }
        else if (sender.State == AudioPlaybackConnectionState.Opened)
        {
            ConnectionState.Text = "Connected";
        }
        else
        {
            ConnectionState.Text = "Unknown";
        }
    });
}

释放连接并处理已删除的设备

此示例提供了“释放音频播放连接”按钮,以允许用户释放音频播放连接。 在此事件的处理程序中,我们获取当前选定的设备,并使用设备的 ID 在字典中查找 AudioPlaybackConnection。 调用 Dispose 以释放引用并释放任何关联的资源,并从字典中删除连接。


private void ReleaseAudioPlaybackConnectionButton_Click(object sender, RoutedEventArgs e)
{
    // Check if an audio playback connection was already created for the selected device Id. If it was then release its reference to deactivate it. 
    // The underlying transport is deactivated when all references are released. 
    if (!(DeviceListView.SelectedItem is null))
    {
        var selectedDeviceId = (DeviceListView.SelectedItem as DeviceInformation).Id;
        if (audioPlaybackConnections.ContainsKey(selectedDeviceId))
        {
            AudioPlaybackConnection connectionToRemove = audioPlaybackConnections[selectedDeviceId];
            connectionToRemove.Dispose();
            this.audioPlaybackConnections.Remove(selectedDeviceId);

            // Notify that the media device has been deactivated. 
            ConnectionState.Text = "Disconnected";
            OpenAudioPlaybackConnectionButtonButton.IsEnabled = false;
        }
    }
}

你应处理设备在启用或打开连接时被删除的情况。 为此,请为设备观察程序的 DeviceWatcher.Removed 事件实现一个处理程序。 首先,使用已删除设备的 ID 从绑定到应用的 ListView 控件的可观察集合中删除设备。 接下来,如果与此设备关联的连接在应用的字典中,则调用 Dispose 以释放关联的资源,然后从字典中删除连接。 以上所有操作都是在对 Dispatcher.RunAsync 的调用中完成的,以确保在 UI 线程上执行 UI 更新。

private async void DeviceWatcher_Removed(DeviceWatcher sender, DeviceInformationUpdate deviceInfoUpdate)
{
    // Collections bound to the UI are updated in the UI thread. 
    await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        // Find the device for the given id and remove it from the list. 
        foreach (DeviceInformation device in this.devices)
        {
            if (device.Id == deviceInfoUpdate.Id)
            {
                this.devices.Remove(device);
                break;
            }
        }

        if (audioPlaybackConnections.ContainsKey(deviceInfoUpdate.Id))
        {
            AudioPlaybackConnection connectionToRemove = audioPlaybackConnections[deviceInfoUpdate.Id];
            connectionToRemove.Dispose();
            this.audioPlaybackConnections.Remove(deviceInfoUpdate.Id);
        }
    });
}

媒体播放