UWP - Casting Technologies - 1

Windows 10 支援 casting 技術讓 App 可以映射 video, audio, screen 到其他支援的屏幕上面,目前提供讓用戶可以快速轉換他們的 small screen 到 big screen 的 casting 技術有幾個協定:DLNA, DIAL, Miracast, Bluetooth, ... 等。該篇將好好介紹怎麽使用。

在 Windows 10 要 casting 到外部屏幕,可常用的方式如下:

  • System Level Connections

(如左圖)提供讓 Windows 設備可以與 external screen 或 audio endpoint 做連綫。

連接 external screen 的方式有:Cable, Miracast;播放 Audio 的連接方式有:Cable, Miracast, Bluetooth。

然而 Miracast 在 Windows 8.1 支援 Miracast (類似 Wireless Projection Technology),在 Windows 10 改善并實現了更多的特性與功能:Better user experience,Telemetry,Reliability imporvements,OS enhancements using standard Miracast and Wi-Fi Direct 等,可以藉由下圖説明:

 

  • Application Level Connections

這個部分分成 3 種類型來説明,如下圖:

Media Element casting:提供 ImageElement, MediaElement 這類型的元素單使用 Casting APIs。

Remote App lanching:使用 DIAL APIs 在另一個設備啓動并執行 application。

Multi-View Applications:利用 ProjectionManager APIs 建立 multi-view screen 的體驗,讓 application 可以同時從一個 Windows 10 設備連接并管理多個 monitors。

 

往下逐一說明每一種類型使用的方式:

  • Media Element casting:

目前 Windows.Media.Casting namespace 讓 App 可以 cast 或 send 特點的 media content 到第二設備,目前支援的範圍有:

  • protocol:Miracast, DLNA, DIAL, Bluetooth
  • media content:ImageElement, MediaElement 與 HTML Tags 中的 images, audio, video

在 Windows 10 支援 MediaElement 内建的 Transport Controls 就有 casting 的按鈕,并且利用 PlayToManager  將内容映射到接收端,也允許 App 自定義 Casting UI 來顯示目前可被連接的設備清單。

藉由下面的 code 來説明:

1. 使用 MediaElement 的 casting to device 按鈕:

<MediaElement x:Name="player" Source="ms-appx:///Assets/Videos/video.mp4" AreTransportControlsEnabled="True" Width="300" />

這是最簡單的方式,只要開啟 MediaElement 的 AreTransportControlsEnabled 就可以使用到內建 Casting to Device 的功能。

 

2. 搭配 CastingDevicePicker 自訂一個 casting 按鈕:

<StackPanel Grid.Row="2">
   <TextBlock Text="Sample2, use custom casting button"/>
   <Button Click="Button_Click">
       <FontIcon Glyph="&#xEC15;" x:Name="castingIcon" />
   </Button>
</StackPanel>
private async void Button_Click(object sender, RoutedEventArgs e)
{
    player.Pause();
    InitialCastingPicker();
    if (connection == null)
    {
         // 從按下的 button 出現 picker 内容
         Button btn = sender as Button;
         GeneralTransform transform = btn.TransformToVisual(Window.Current.Content as UIElement);
         Point pt = transform.TransformPoint(new Point(0, 0));
         picker.Show(new Rect(pt.X, pt.Y, btn.ActualWidth, btn.ActualHeight), Windows.UI.Popups.Placement.Above);
    }
    else
    {
         // 關掉現在的連綫,要記得去掉 event 注冊以免發生 memory leak
         connection.ErrorOccurred -= Connection_ErrorOccurred;
         connection.StateChanged -= Connection_StateChanged;
         await connection.DisconnectAsync();
         connection.Dispose();
         connection = null;
    }
}
private async void Picker_CastingDeviceSelected(CastingDevicePicker sender, CastingDeviceSelectedEventArgs args)
{
    await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
    {
        connection = args.SelectedCastingDevice.CreateCastingConnection();
	//Hook up the casting events
	connection.ErrorOccurred += Connection_ErrorOccurred;
	connection.StateChanged += Connection_StateChanged;

	// Get the casting source from the MediaElement
	CastingSource source = null;

	try
	{
            // Get the casting source from the Media Element
            source = player.GetAsCastingSource();

            // Start Casting
            CastingConnectionErrorStatus status = await connection.RequestStartCastingAsync(source);

            if (status == CastingConnectionErrorStatus.Succeeded)
	    {
		player.Play();
	    }
	}
	catch
	{

	}
    });
}

 

主要利用 CastingDevicePicker 找出可用的設備 (設定 Filter 找出支援目前播放媒體的設備),接著按照用戶選擇的設備建立 CastConnection 并且把内容 casting 到設備上。從 CastConnection 的 StatusChanged 事件去控制目前連綫狀況。

要記得 CastConnection 沒有使用就要做 DisconnectAsync 跟 Dispose 讓連綫斷掉,以免下一次連綫都會失敗。

 

負責列出可以被 casting 的設備清單,提供給用戶選擇。

Type Name Description
event CastingDevicePickerDismissed Indicates that the user has dismissed the picker UI.
  CastingDeviceSelected Indicates that the user has selected a device from the picker.
methods Hide Hides the device picker UI.
  Show(Rect) Shows the casting device picker UI, which flies out from an edge of the provided rectangle.
  Show(Rect, Placement) Shows the casting device picker UI, which flies out from the specified edge of the provided rectangle.
Property Appearance Read-Only. Gets the colors of the picker UI.
  Filter

Read-Only. Gets the filter information for which devices to show in the picker.

Filter 的設定會影響可以被列在可被 casting 的設備清單。每一項 Filter 的設定會使用 OR-ed 的邏輯來搜尋可以用的設備。

例如:設定 SupportAudio 與 SupportVideo 的話, Audio-only devices, video-only devices, 與 audio/video devices 這些都會被顯示出來。

 

代表可以被 casting 到其他設備的 media content。

Name Description
PreferredSourceUri Gets or sets an alternative URI for the content for use with DLNA ByRef.

 

Type Name Description
Event ErrorOccurred Indicates an error occurred while attempting to make a casting connection.
  StateChanged

Indicates a change in the State property. 使用 CastingConnectionState 内容:

  • 0: disconnected
  • 1: connected
  • 2: rendering
  • 3: disconnecting
  • 4: connecting
Method Close Closes the casting connection.
  DisconnectAsync Terminates a casting connection.
  RequestStartCastingAsync Starts the process of casting to a casting device.
Properties Device

Read-only. Gets the casting device with which a connection has been made. 得到 CastDevice 物件。

  Source Read/Write. Gets and sets the content source that is being casted through the connection to the casting device. The content can be set and changed at any time and doing so does not disconnect the connection.
  State Read-only. Gets the current state of the connection.

上述範例是分開 casting 按鈕使用,如果希望直接跟 MediaElement 整合的話,可以參考<Create custom transport controls>,<Media Transport Controls sample>。

 

 

  • Remote App launching

利用 DIAL(DIscovery And Launch) API 從 Windows device 執行在遠端設備(例如:Xbox One 或是其他 DIAL capable devices) 中可以相呼應的應用程式。

運作流程大致如下圖:

重點在于對方的設備要有支援 DIAL,并且要有對應的 App,不然衹能簡單的 casting content,無法做到而外的控制。

DIAL 具有兩種角色,分別説明并且介紹需要用到的 API :

  • Sender:目前 API 支援 Desktop/Mobile ,支援 DIAL 1.6.4;

負責對 Receiver 發送訊息,任務就是從 DialDevicePicker 中設定有支援指定 app name 的 Receiver。 app name 是 sender/receiver 互相定義的。

以下介紹幾個重要的元件:

負責列出有支援 Dial 協定,并且符合所設定的 App Name 的設備清單,提供給用戶選擇。

Type Name Description
Events DialDevicePickerDismissed Indicates that the device picker was light dismissed, which means that the user clicked or touched anywhere other than the picker UI, and so the picker will be closed.
  DialDeviceSelected Indicates that the user selected a device from the picker.
  DisconnectButtonClicked Indicates that the user clicked on the disconnect button in the picker.
Methods SetDisplayStatus

Updates the picker UI to reflect the status fo a given remote device.

設定 DialDevice 目前的狀態。主要就是通知 DailDevice 有 Sender 要準備跟他連接或是其他動作。以 DialDeviceDisplayStatus 的列舉值。

  PickSingleDialDeviceAsync Shows the picker. Call this method directly to show to show the picker, instead of showing it in response to an event.
  Show Displays the picker to the user. When called, the picker flies out from an edge of the provided rectangle.
Properties Appearance Used to change the colors of the picker.
  Filter

Gets the filter used to choose what devices to show in the picker.

設定 DialDevicePickerFilter 中的 SupportedAppNames 。預設是 empty 代表全部設備都搜尋。設定之後就會從有支援 DIAL 設備裏面找有支援指定 App 的。

使用方式跟 CastingDevicePicker 相似。衹是要特別主要 SetDisplayStatus 通知 Reciver 目前的狀態,例如:connecting, disconnect 等。

 

代表支援 DIAL 的設備,利用 FromIdAsync 找到這個設備的識別值後,再利用 GetDialApp 抓到在 DialDevicePicker.Filter 設定 AppName 的 App。

Method Description
DeviceInfoSupportDialAsync Indicates whether or not the device supports launching DIAL apps.
FromIdAsync Returns a DialDevice object for a given a device ID (acquired from a query using the Windows.Devices.Enumeration APIs).
GetDeviceSelector Returns an AQS filter string to be used with the Windows.Devices.Enumeration APIs (such as the CreateWatcher API) for a given Dial app.
GetDialApp Creates a new DialApp object. This method does not establish a connection to the device or validate that the app exists. That is done when any function is called on the resulting DialApp object.

 

代表執行在遠端設備中 DIAL App。利用 RequestLanuchAsync 傳送資料給遠端設備。擔任 Receiver 的 App 會觸發 DialProtocol 的 OnActivated 事件。

Method Description
GetAppStateAsync Gets the current status of the application on the remote device.
RequestLaunchAsync

Initiates the launching of the app on the remote device.

使用這個 method 前還沒有跟設備做配對,系統會提示用戶要先建立連綫并且被驗證,通過之後才會正常執行。

得到回傳值:DialAppLaunchResult

StopAsync Stops the app on the remote device, if the remote device supports this functionality. 這個要特別注意,不是所有設備都會實做 Stop 的機制,而且有可能在呼叫 Stop 的時候因爲網路或是其他因素造成失敗。可以加入 retry 的機制。

 

範例説明:

1. 初始化 DailDevicesPicker 與設定要取得的 AppName:

private void InitDialDeivcePicker()
{
   if (picker == null)  
   {
       picker = new DialDevicePicker();
       // 設定支援的 app name
       picker.Filter.SupportedAppNames.Add("castingsample");
       picker.DialDevicePickerDismissed += Picker_DialDevicePickerDismissed;
       picker.DialDeviceSelected += Picker_DialDeviceSelected;
       picker.DisconnectButtonClicked += Picker_DisconnectButtonClicked;
   }
}

2. 定義處理 DialDeviceSelected 事件發生時要與遠端設備連綫,并送參數過去:

private async void Picker_DialDeviceSelected(DialDevicePicker sender, DialDeviceSelectedEventArgs args)
{
    // casting 必須在 UI Thread 下執行
    await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
    {
	try
	{
	    // 設定遠端設備現在要準備連綫
	    picker.SetDisplayStatus(args.SelectedDialDevice, DialDeviceDisplayStatus.Connecting);

	    // 取得遠端設備中支援指定 app name 的 App
	    DialApp app = args.SelectedDialDevice.GetDialApp(txtAppName.Text);

	    if (app == null)
	    {
	       // 嘗試建立 DIAL device,如果失敗代表那個設備不支援 DIAL
	       picker.SetDisplayStatus(args.SelectedDialDevice, DialDeviceDisplayStatus.Error);
            }
	    else
	    {
	       // 請求送出參數到遠端設備的 App 
	       DialAppLaunchResult result = await app.RequestLaunchAsync(txtArgument.Text);
	        		
	       if (result == DialAppLaunchResult.Launched)
	       {
		    activeDialDevice = args.SelectedDialDevice;
		    DeviceInformation selectedDeviceInformation = await DeviceInformation.CreateFromIdAsync(args.SelectedDialDevice.Id);

                    activeDeviceInformation = selectedDeviceInformation;
		    picker.SetDisplayStatus(activeDialDevice, DialDeviceDisplayStatus.Connected);
                    picker.Hide();
		    tblMsg.Text += "device connected";
	       }
	       else
	       {
		    picker.SetDisplayStatus(args.SelectedDialDevice, DialDeviceDisplayStatus.Error);
                    tblMsg.Text += "device error";
	       }
	    }
	}
	catch (Exception ex)
	{
	    tblMsg.Text += ex.Message;
	}
    });
}

3. 處理用戶點擊了 disconnect 按鈕的事件,需要請求關閉連綫:

private async void Picker_DisconnectButtonClicked(DialDevicePicker sender, DialDisconnectButtonClickedEventArgs args)
{
    // casting 必須在 UI Thread 下執行
    await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
    {
	try
	{                    
	    // 取得被選擇的 dial device
	    DialDevice selectedDialDevice = await DialDevice.FromIdAsync(args.Device.Id);
	    // 更新 picker status
	    picker.SetDisplayStatus(selectedDialDevice, DialDeviceDisplayStatus.Connecting);
	    // 取得 dial app 
	    DialApp app = selectedDialDevice.GetDialApp(txtAppName.Text);
	  
	    // 請求斷綫
	    DialAppStopResult result = await app.StopAsync();

	    if (result == DialAppStopResult.Stopped)
	    {
		picker.SetDisplayStatus(args.Device, DialDeviceDisplayStatus.Disconnected);
		activeDialDevice = null;
		activeDeviceInformation = null;
		picker.Hide();
		tblMsg.Text += "Stoped, success";
	    }
	    else
	    {
		if (result == DialAppStopResult.StopFailed || result == DialAppStopResult.NetworkFailure)
		{
		     // 如果失敗的話要記得多 retry 的機制
		     picker.SetDisplayStatus(args.Device, DialDeviceDisplayStatus.Error);
		     tblMsg.Text += $"Stoped, {result}";
		}
		else
		{
		    // 如果設備沒有支援 Stop 機制,則直接清楚連綫就好
		    activeDialDevice = null;
		    activeDeviceInformation = null;
		    tblMsg.Text += "the device does not support Stop";
		}
	    }
	}
	catch (Exception ex)
	{
            tblMsg.Text += ex.Message;
	}
    });
}

 

 

  • Receiver:Xbox One 支援 DIAL 1.6.4,在 Xbox One 上的 YoutuBe / Netfix 的 app 也有支援 DIAL;

擔任 Receiver 的 App 要在 Package.appxmanifest 宣告支援 Dial protocol,并且定義名稱 (AppName),如下圖:

接著,注冊處理 App.xaml.cs 中的 OnActivated 事件,從 ActivationKind.DialReceiver 來處理收到的參數:

protected override void OnActivated(IActivatedEventArgs args)
{
    base.OnActivated(args);
    // 判斷是否爲 DialReceiver
    if(args.Kind == ActivationKind.DialReceiver)
    {
        // 如果是直接啓動 app 可能會沒有畫面要先建立
        var rootFrame = Window.Current.Content as Frame;
        if (rootFrame == null)
        {
            rootFrame = new Frame();
            rootFrame.NavigationFailed += OnNavigationFailed;
            Window.Current.Content = rootFrame;
        }
        rootFrame.Navigate(typeof(DialReceiverPage), args);
    }
    Window.Current.Activate();
}

收到的參數爲 DialReceiverActivatedEventArgs,説明如下:

Property Description
AppName Gets the name of the app that invoked the dial receiver app.
Arguments Gets the arguments passed by the calling app.
CurrentlyShownApplicationViewId Gets the identifier for the currently shown app view.
Kind Gets the activation type.
PreviousExecutionState Gets the execution state of the app before it was activated.
SlashScreen Gets the splash screen object, which provides information about the transition from the splash screen to the activated app.
TileId Gets the unique tile identifier for the calling app.
ViewSwitcher Gets the view switcher object that allows you to set the view for the application.

要找到支援 DIAL 的設備比較辛苦,如果你本身有 Xbox one 就可以直接測試。

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);
    // 轉換參數爲 DialReceiverActivatedEventArgs
    DialReceiverActivatedEventArgs args = e.Parameter as DialReceiverActivatedEventArgs;
    if (args == null)
    {
        txtArgument.Text = "not from dial sender.";
    }
    else
    {
        txtArgument.Text = args.Arguments;
    }
}

 

 

  • Combine Application Level methods

上述介紹了如何使用 MediaElement 的 casting,DIAL 的 remote app lanuching , 當然不止有這樣而已,還有 ProjectionManager 的 Multi-View applications 與直接開發一個 Combine 的方式將這些支援的機制都整合在一起。可以繼續閲讀<UWP - Casting Technologies - 2>。

 

[範例代碼]

 

[補充]

  • DLNA (Digital Living Network Alliance)

定義家中消費電子,行動電話,電腦等如何 connected devices 與 streaming media 的協定。如果您的設備要支援 DLNA 需要有 DLAN.org 的認證才可以。關於在 Windows 10 的測試可參考<Testing DIAL, DLNA and Miracast on Windows 10>。

  • DIAL (Discovery And Launch)

使用 Netfix 與 Youtube 定義的標準,主要是讓另一個 screen 變成第二個 screen,藉由 first-screen devices 來操作。 常見的就是 Google Chromecast。

採用 Wi-Fi Direct 為基礎的無線顯示標準,支援的設備可以無綫分享多媒體内容。

  • 如何找到 Xaml Element 的 temlplate

For more info about modifying templates, see [Control templates](). You can use a text editor or similar editors in your IDE to open the XAML files in (Program Files)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\(SDK version)\Generic. The default style and template for each control is defined in the generic.xaml file. You can find the MediaTransportControls template in generic.xaml by searching for "MediaTransportControls".

======

上述介紹的 casting 很適合用在 App 是開發多媒體或是生產力工具的 App,因爲 Windows 10 支援設備整合 Bluetooh 的 mouse/keyboard,讓 App 可以把適合的内容 castting 到特定的設備上,能增加 App 的使用率與用戶的方便性。

希望對大家有所幫助,謝謝。

 

References: