stream comics from windows 8 to other devices using playto

There are plenty of blogposts, tutorials, videos, books and many more out there that talk about searching, sharing settings and if you’re lucky even printing. But the Devices charm can be used for something way cooler than printing some pages, it can trigger the Play To contract.

The Play To contract can share media like music, video and pictures to other devices on your network, be it other Windows 8 devices or even Xbox 360 consoles. Being a comic geek I the first thing on my mind when reading about the Play To contract was “this would be awesome to stream comics from my pc to my Xbox” and so a challenge was born and accepted on the same day. An evening or two of hacking later I’m proud to say that I did it and it’s really easy, just like everything that involves charms (Microsoft really did a good job on allowing us to integrate our apps with the OS here). Most of the time putting this thing together went to building the actual comic viewer, but enough talk, let’s take a look at how it’s done. First let me show you how it looks like, we’ll start with the app itself.

Pretty empty so far, if we load a digital comic (only .cbz format supported in this demo) and select the Devices charm we get this.

That’s right! that’s Captain America himself, including Bucky. And oh yeah, the xbox shows up in the Devices charm as well. After selecting the xbox from the list of devices we get this.

And here it is playing on my TV (yes that’s a Sony, because Microsoft doesn’t build televisions yet Winking smile)

Pretty cool eh? Let’s check under the hood.

First, a digital comic mostly exists in either .cbz or .cbr format. They’re actually nothing more then a zip or a rar file (Comic Book Zip and Comic Book Rar). Since WinRT has the ZipArchive class we can support .cbz out of the box, for cbr format we would need to find a library that supports rar files but that’s outside the scope of this post.

First the XAML, the main control here is a FlipView, that allows for touch and mouse support out of the box. The FlipView is bound to a collection of bitmap images that get loaded from the digital comic. Next to the FlipView there’s also an appbar containing the load file button and a textblock that shows the connection to other devices.

   1: <common:LayoutAwarePage.BottomAppBar>
   2:     <AppBar IsOpen="True" Background="#FF1A76B6">
   3:         <StackPanel Orientation="Horizontal">
   4:             <Button Click="OpenButton_Click" Style="{StaticResource OpenFileAppBarButtonStyle}" />
   5:         </StackPanel>
   6:     </AppBar>
   7: </common:LayoutAwarePage.BottomAppBar>
   9: <Grid Style="{StaticResource LayoutRootStyle}">
  10:     <Grid.RowDefinitions>
  11:         <RowDefinition Height="140"/>
  12:         <RowDefinition Height="*"/>
  13:     </Grid.RowDefinitions>
  15:     <TextBlock x:Name="ConnectionText" TextWrapping="Wrap" Text="Not connected" Margin="38,18,766,571" 
  16:                Grid.Row="1" Style="{StaticResource SubheaderTextStyle}"/>
  18:     <FlipView x:Name="FlipImage" Margin="0,3,0,0" Grid.RowSpan="2" SelectionChanged="FlipImage_NextImage">
  19:         <FlipView.ItemTemplate>
  20:             <DataTemplate>
  21:                 <Image Source="{Binding}" />
  22:             </DataTemplate>
  23:         </FlipView.ItemTemplate>
  24:     </FlipView>
  26:     <Grid>
  27:         <Grid.ColumnDefinitions>
  28:             <ColumnDefinition Width="Auto"/>
  29:             <ColumnDefinition Width="*"/>
  30:         </Grid.ColumnDefinitions>
  31:         <Button x:Name="backButton" Click="GoBack" IsEnabled="{Binding Frame.CanGoBack, ElementName=pageRoot}" 
  32:                 Style="{StaticResource BackButtonStyle}"/>
  33:         <TextBlock x:Name="pageTitle" Grid.Column="1" Text="{StaticResource AppName}" 
  34:                    Style="{StaticResource PageHeaderTextStyle}"/>
  35:     </Grid>
  36: </Grid>

The FlipView template is just a simple Image control, nothing special in the XAML, the magic is in the code behind.

First some fields that we’ll need later on

   1: private List<BitmapImage> _pages;
   2: private bool _isConnected;
   3: private Image _current;

We’re going to take a look at how to load the cbz file first, I’ll go over this quickly as the main focus of this post is the PlayTo contract.

   1: private async void OpenButton_Click(object sender, RoutedEventArgs e)
   2: {
   3:     _pages = new List<BitmapImage>();
   5:     _pages = await OpenZip();
   7:     if (_pages.Count > 0)
   8:         FlipImage.ItemsSource = _pages;
   9: }

All we do in the button’s eventhandler is initializing the field that will hold all the pages, call the function that will load the file and if it contains any items it will set the FlipView’s itemssource to that list. Next up: the function to load the comic.

   1: async Task<List<BitmapImage>> OpenZip()
   2: {
   3:     FileOpenPicker openPicker = new FileOpenPicker();
   4:     List<BitmapImage> comic = new List<BitmapImage>();
   6:     openPicker.SuggestedStartLocation = PickerLocationId.ComputerFolder;
   7:     openPicker.FileTypeFilter.Add(".cbz");
   9:     var storageFile = await openPicker.PickSingleFileAsync();
  10:     // Create stream for compressed files in memory
  11:     using (MemoryStream zipMemoryStream = new MemoryStream())
  12:     {
  13:         using (IRandomAccessStream zipStream = await storageFile.OpenAsync(FileAccessMode.Read))
  14:         {
  15:             // Read compressed data from file to memory stream
  16:             using (Stream instream = zipStream.AsStreamForRead())
  17:             {
  18:                 byte[] buffer = new byte[1024];
  19:                 while (instream.Read(buffer, 0, buffer.Length) > 0)
  20:                 {
  21:                     zipMemoryStream.Write(buffer, 0, buffer.Length);
  22:                 }
  23:             }
  24:         }
  27:         // Create zip archive to access compressed files in memory stream
  28:         using (ZipArchive zipArchive = new ZipArchive(zipMemoryStream, ZipArchiveMode.Read))
  29:         {
  30:             // For each compressed file...
  31:             foreach (ZipArchiveEntry item in zipArchive.Entries)
  32:             {
  33:                 if (item.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase))
  34:                 {
  35:                     byte[] imageInBytes;
  37:                     using (MemoryStream ms = new MemoryStream())
  38:                     {
  39:                         var stream = item.Open();
  40:                         stream.CopyTo(ms);
  41:                         imageInBytes = ms.ToArray();
  42:                     }
  44:                     BitmapImage bImg = new BitmapImage();
  45:                     await bImg.SetSourceAsync(new RandomStream(imageInBytes));
  47:                     comic.Add(bImg);
  48:                 }
  49:             }
  50:         }
  51:     }
  53:     return comic;
  54: }

We start by initializing the FileOpenPicker, allowing our user to select the comic he/she wants to read. We add a suggested location where the FileOpenPicker should start and add the filetypes it should look for. The PickFileAsync method shows the actual filepicker to the user, the user selects the cbz file he wants and it gets loaded into the storageFile variable. The file gets read in as an IRandomAccessStream. We need that stream to create a ZipArchive instance. Once we have that we can loop through all files in that zip archive. Each .jpg file in that zip archive gets read into a byte array that we then convert into a bitmap by using RandomStream, an implementation of IRandomAccessStream (if you want to see the implementation, the project is attached to this post at the bottom). The bitmap image then gets added to the list. When they’re all done the list gets returned to the caller.

That’s it for loading the comic, let’s take a look at the sharing to other devices in your network.

We need to initialize the PlayTo contract, I’ll be doing this from the constructor

   1: public MainPage()
   2: {
   3:     InitializeComponent();
   5:     var playToManager = PlayToManager.GetForCurrentView();
   6:     playToManager.SourceRequested += PlayToManagerOnSourceRequested;
   7:     playToManager.SourceSelected += PlayToManagerOnSourceSelected;
   8: }

PlayToManager is the class that we need, we get this for free from the WinRT framework. The GetForCurrentView() method returns an instance of the PlayToManager class bound to this page. Once we have the instance we attach an eventhandler to the SourceRequested and the SourceSelected events. The SourceRequested event will fire as soon as the user hits the Devices charm, this is where we’ll prepare the first media element for streaming. The SourceSelected event fires when the user selects a source, obviously.

   1: private async void PlayToManagerOnSourceRequested(PlayToManager sender, PlayToSourceRequestedEventArgs args)
   2: {
   3:     var deferral = args.SourceRequest.GetDeferral();
   5:     await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
   6:         {
   7:             var firstImage = GetChildren(FlipImage).First();
   9:             // Provide Play To with the first image to stream.
  10:             args.SourceRequest.SetSource(firstImage.PlayToSource);
  11:             _current = firstImage;
  12:             deferral.Complete();
  13:         });
  15: }

The OnSourceRequested eventhandler needs to be marked as async. First we get the deferral, then we need to run some async code, Dispatcher.RunAsync is the same as calling an async method with await on this line. The PlayTo contract works with certain XAML controls. In our case we need the Image control, that’s why we’ve set the itemtemplate of the FlipView to be an Image. We’ll take a look at the GetChildren() method in a minute, for now just know that it returns a list of all Image controls inside the FlipView. We take the first element in the returned list and that’s the element that we’ll stream to the device. The arguments have a property of type PlayToSourceRequest, that one has a SetSource function that takes in a PlayToSource object and that’s a property of the Image control. We set the current image to the _current field and mark the deferral as complete.

Phew that was quite some work. Don’t worry, the hard part is over (yes this was really the hard part). Now a quick look at that GetChildren() function.

   1: private List<Image> GetChildren(DependencyObject parent)
   2: {
   3:     var list = new List<Image>();
   5:     for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
   6:     {
   7:         var child = VisualTreeHelper.GetChild(parent, i);
   8:         var item = child as Image;
   9:         if (item != null)
  10:             list.Add(item);
  12:         list.AddRange(GetChildren(child));
  13:     }
  15:     return list;
  16: }

The function takes in a DependencyObject and starts walking through its visual tree. We try to cast each item as an Image, if that cast fails the variable will contain null, a quick check there and if it isn’t null we add it to the list which we then return.

The OnSourceSelected is only used to set the name of the selected source to the textblock

   1: private async void PlayToManagerOnSourceSelected(PlayToManager sender, PlayToSourceSelectedEventArgs args)
   2: {
   3:     _isConnected = true;
   5:     await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
   6:         ConnectionText.Text = string.Format("Connected to {0}", args.FriendlyName));
   7: }

And that’s enough to stream the first image, when you run this code and select the Devices charm the first page of the comic should show up on your device. All there’s left now is to go back and forward in the comic. In the app itself this already works, the FlipView takes care of navigating between the pages. Before we start developing this we need to make a small halt and take a look at how the FlipView actually works. First thought in my head was “okay this is easy, I just get all the image controls in the FlipView and I’m golden”. That was a big nono. The itemspanel of a FlipView is actually a VirtualizingStackPanel, meaning that at any given time there are maximum three Image controls inside the FlipView, usually previous-current-next. As soon as we navigate to another page the FlipView automatically loads in the next item in line. This can easily be seen by using a handy tool called XamlSpy. XamlSpy allows us to view the entire visual tree of any XAML based application. When we view the default visual tree of a FlipView after loading a comic we get this.

As you can see, we only have three FlipViewItems here. When we change the FlipView’s paneltemplate to this

   1: <FlipView x:Name="FlipImage" Margin="0,3,0,0" Grid.RowSpan="2" SelectionChanged="FlipImage_NextImage">
   2:     <FlipView.ItemsPanel>
   3:         <ItemsPanelTemplate>
   4:             <StackPanel />
   5:         </ItemsPanelTemplate>
   6:     </FlipView.ItemsPanel>
   7:     <FlipView.ItemTemplate>
   8:         <DataTemplate>
   9:             <Image Source="{Binding}" />
  10:         </DataTemplate>
  11:     </FlipView.ItemTemplate>
  12: </FlipView>

and we load the same comic in XamlSpy we get this result

Quite the difference I would say. The VirtualizingStackPanel is lighter on memory usage, since some comics can be quite large we’ll stick to the default template.

Now that we have that cleared out, let’s take a look at what happens when we browse to the next page of the comic.

   1: private async void FlipImage_NextImage(object sender, SelectionChangedEventArgs e)
   2: {
   3:     if (!_isConnected) return;
   5:     await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
   6:         {
   7:             var current = GetChildren(FlipImage)[1];
   9:             _current.PlayToSource.Next = current.PlayToSource;
  10:             _current.PlayToSource.PlayNext();
  11:             _current = current;
  12:         });
  13: }

First, this code does not need to be executed when we’re not connected to any device, if we are we need to run the next block of code asynchronously. First the code fetches all the available Image controls inside the FlipView, we save a reference to the middle one because that one is the one currently shown in the FlipView. The field _current contains the image currently shown on the external device, we need to set that field’s PlayToSource.Next property. That property always needs to be set on the current image before the PlayNext() method is called. Once that’s set we call the aforementioned PlayNext() method. That method will sent the control that is set to the PlayToSource.Next property to the connected device. To end we set the image control that was just send to the device to the _current field so that it can be called upon on the next run.

In this post I have shown how you can share media content from your Windows Store application to an external device like the Xbox 360. The project used in this post can be downloaded from my SkyDrive

This is an imported post. It was imported from my old blog using an automated tool and may contain formatting errors and/or broken images.

Leave a Comment