はじめに
前回 に引き続きLive FrameItのSDKを利用した仮想フォトフレームアプリケーションの作成です。今回はユーザーのコレクションを取得してLive FrameItから配信される画像を取得・表示まで行います。また、今回は仮想フォトフレームとなる図1 のようなアプリケーションを作成しながら進めます。
図1 仮想フォトフレーム
作成するサンプルアプリケーションのソースコードは、この記事の最終ページからダウンロードできます。
登録トークンを使用したデイバスIDの取得
前回 、デバイスをLive FrameItに登録するために必要なデバイスIDの取得方法を紹介しました。その際にWindows Live IDアカウントによるユーザー認証をデバイス(またはアプリケーション)側で実装するものと、Live FrameItのWebサイトで行う方法によるもののうち、前者のコードを示しました。今回の最初は後者のコードを簡単に紹介します。Windows Live ID Client SDKを使用しないため、たいていのプログラミング言語で実装が可能です。ただし紹介するコードは前回同様VB.NETです。
デバイス側でLive IDアカウントによるユーザー認証を行わない代わりに登録トークンという文字列を使用してデバイスをLive FrameItへ登録します。詳しい手順は前回 を参照してください。
登録トークンの取得
まずデバイスは、登録トークンをLive FrameItサービスへ要求し取得します。Webサービスを呼び出すためにサービス参照を追加する必要があります。こちらも詳しくは前回 を参照してください。登録トークンの取得には、サービス参照によって作成されたクラスDeviceSvcSoapClientクラスのGetClaimToken メソッドを使用します。メソッドの引数は、製造元の名前とシリアル番号です。いずれも64文字以下で自由に設定します。
Dim client = New DeviceSvcSoapClient
Dim manufacturerId = "Virtual Photo Frame"
Dim serialNumber = Now.Ticks.ToString
Dim result = client.GetClaimToken(manufacturerId, serialNumber)
Dim calimToken = result.ClaimToken
GetClaimTokenメソッドの戻り値は、GetClaimTokenResults型です。このクラスの持っているClaimTokenプロパティを参照すると登録トークンが取得できます。この取得したトークンをユーザーに提示し、Live FrameIt Webサイト(図2 )でトークンをユーザーに入力してもらいます。GetClaimTokenResults.ClaimUrlプロパティを参照するとユーザーがトークンを入力するWebページのアドレスを取得できます。
図2 登録トークンの入力
デイバスIDの取得
ユーザーによる登録トークンの入力が完了すると、登録トークン要求時に指定した製造元の名前とシリアル番号および取得した登録トークンを使用して、デバイスIDが取得できます。取得にはDeviceSvcSoapClientクラスのDeviceBind メソッドを使います(前回はDeviceBindUserメソッドを使いました) 。Live IDアカウントのユーザー認証がないためHTTPヘッダーフィールドの追加など前回 のような複雑な処理は必要ありません。以下に一連のコードを示します。
Private Sub RegisterDeviceWithToken()
Dim result As DeviceBindResults
Using client = New DeviceSvcSoapClient
Dim manufacturerId = "Virtual Photo Frame"
Dim serialNumber = Now.Ticks.ToString
Dim tokenResult = client.GetClaimToken(manufacturerId, serialNumber)
If tokenResult.ResponseCode <> 0 Then
Exit Sub
End If
MessageBox.Show(tokenResult.ClaimUrl & " へアクセスして登録トークン " & _
tokenResult.ClaimToken & " を設定してください。" & vbCrLf & _
"設定完了後 OK ボタンをクリックしてください。" )
result = client.DeviceBind(tokenResult.ClaimToken, manufacturerId, serialNumber)
End Using
If result.ResponseCode = 0 Then
My.Settings.DeviceId = result.DeviceId
My.Settings.Save()
End If
End Sub
UIの作成
デバイスIDの取得後は、コレクションを取得し、コレクションの各画像を取得・表示処理という流れなりますが、ここで冒頭の図でも見せた仮想フォトフレームのUIを作成しようと思います。本連載の趣旨とは少しはずれますので簡単に済ませて次に進みましょう。作るのはLive FrameItから配信された画像を表示する最低限程度のものとなっています。これを参考にまたは独自に作りこんで頂けると幸いです。
プロジェクトの作成
Visual StudioのプロジェクトはWPFアプリケーションを選択します(図3 ) 。必要なWebサービス参照の追加などはこれまでの内容を元に設定してください。
図3 WPFアプリケーションプロジェクトの作成
デザイン
見た目の部分はXAMLファイルに記述します。プロジェクト作成後にはWindow1.xamlというファイルがあるので、これの名前を変更(ファイル名だけでなくクラス名も変更する)して編集しています。図4 にデザイナ画面を示します。
図4 デザイナ画面
今回作成するアプリケーションは、ウィンドウを透明にし、その上に取得した画像と枠となる画像を重ねて表示しています。また、終了や表示するコレクションの選択など操作は右クリックメニューに用意しています(図5 ) 。
図5 右クリックメニュー
以下にXAMLファイルの内容を示します。ウィンドウや画像の大きさや位置は、用意した画像に併せて適当に設定します。
< Window x:Class = "FrameWindow"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:me = "clr-namespace:VirtualPhotoFrame"
Title = "仮想フォトフレーム" Width = "800" Height = "750"
AllowsTransparency = "True" WindowStyle = "None" Background = "Transparent" ResizeMode = "NoResize"
MouseLeftButtonDown = "Window_MouseLeftButtonDown" >
< Grid >
< Grid . ContextMenu >
< ContextMenu >
< ContextMenu . CommandBindings >
< CommandBinding Command = "{x:Static me:FrameWindow.SelectCollectionCommand}"
Executed = "SelectCollectionCommand_Executed" />
</ ContextMenu . CommandBindings >
< MenuItem Header = "デバイスの登録" Click = "RegisterMenuItem_Click" />
< MenuItem Header = "コレクションの選択" x:Name = "CollectionMenuItem" >
< MenuItem . ItemContainerStyle >
< Style TargetType = "MenuItem" >
< Setter Property = "Header" Value = "{Binding Name}" />
< Setter Property = "Command" Value = "{x:Static me:FrameWindow.SelectCollectionCommand}" />
< Setter Property = "CommandParameter" Value = "{Binding FeedUrl}" />
</ Style >
</ MenuItem . ItemContainerStyle >
</ MenuItem >
< MenuItem Header = "コレクションの再取得" Click = "GetCollectionMenuItem_Click" />
< Separator />
< MenuItem Header = "終了" Click = "ExitMenuItem_Click" />
</ ContextMenu >
</ Grid . ContextMenu >
< Grid Background = "Black" Margin = "157,170,141,182" Width = "480" Height = "360" />
< Image x:Name = "PhotoImage" Margin = "157,170,141,182" Width = "480" Height = "360" />
< Image Source = "frame.png" />
</ Grid >
</ Window >
ユーザーのコレクションの数によってメニューの項目数も変わります。この部分はWPFの特徴であるバインディングやコマンドという機能を使用して実装します。それ以外のユーザー操作による処理部分はWindows.Formsのときと同様なイベントを用いて処理することにします。
設定の保存
デバイスIDや表示するコレクションのRSSフィードのURLを保存するために、アプリケーション設定機能をします。プロジェクトのプロパティの設定タブから図6 のように保存用の項目を追加します。
図6 アプリケーション設定
DeviceIdとCollectionFeedUrlという項目を追加しました。
コードの記述
本題ではないメニューのクリックイベント処理部分などをまとめて以下に示します。.xaml.vbファイルに記述します。
Imports VirtualPhotoFrame.ServiceReference
Imports Microsoft.WindowsLive.Id.Client
Imports System.Net
Imports System.ServiceModel
Imports System.ServiceModel.Channels
Imports System.ServiceModel.Syndication
Imports System.Threading
Imports System.Windows.Threading
Class FrameWindow
Public Shared ReadOnly SelectCollectionCommand As RoutedCommand = _
New RoutedCommand("SelectCollection" , GetType (FrameWindow))
Private Sub Window_MouseLeftButtonDown(ByVal sender As System.Object , ByVal e As System.Windows.Input.MouseButtonEventArgs)
DragMove()
End Sub
Private Sub RegisterMenuItem_Click(ByVal sender As System.Object , ByVal e As System.Windows.RoutedEventArgs)
RegisterDevice()
End Sub
Private Sub ExitMenuItem_Click(ByVal sender As System.Object , ByVal e As System.Windows.RoutedEventArgs)
Me .Close()
End Sub
…
End Class
この後も、このファイルにコードを追記していきます。
コレクションの取得
それではコレクション情報を取得してみましょう。デバイスIDを取得するとユーザーのコレクションを取得できるようになります。取得には、サービス参照によって作成されたクラスDeviceSvcSoapClientクラスのGetCollectionInfo メソッドを使用します。引数はデバイスIDです。
Using client = New DeviceSvcSoapClient
Dim results = client.GetCollectionInfo("取得したデバイス ID" )
If results.ResponseCode = 0 Then
For Each l In results.CollectionInfoList
Console.WriteLine("Name: {0}, Url: {1}" , _
l.Name, _
l.FeedUrl)
Next
End If
End Using
戻り値のCollectionInfoListプロパティを参照すると、ユーザーのコレクション情報が得られます。コレクション情報には、ユーザーが指定したコレクション名と、Live FrameItが生成したコレクションのRSSフィードのURLが含まれています。それぞれ、CollectionInfoクラスのNameとFeedUrlプロパティとして参照できます。
作成中のアプリケーションのコードには次のようにメソッド化したものを追記しておきましょう。またコレクションの再取得メニューをクリック時にメソッドを呼び出すようにします。
Private Sub GetCollectionInfo()
Using client = New DeviceSvcSoapClient
Dim results = client.GetCollectionInfo(My.Settings.DeviceId)
If results.ResponseCode <> 0 Then
MessageBox.Show("コレクションの取得に失敗しました。" )
Exit Sub
End If
CollectionMenuItem.ItemsSource = results.CollectionInfoList
End Using
End Sub
Private Sub GetCollectionMenuItem_Click(ByVal sender As System.Object , ByVal e As System.Windows.RoutedEventArgs)
GetCollectionInfo()
End Sub
コレクションが取得できたら、コレクション名をユーザーに提示し、デバイスで表示するコレクションを決定します。今回作成するアプリケーションではメニューをクリックすることでコレクションを選択します。以下のコードがコレクションのメニューをクリック時に呼ばれる処理になります。
Private Sub SelectCollectionCommand_Executed(ByVal sender As System.Object , ByVal e As System.Windows.Input.ExecutedRoutedEventArgs)
Dim url = CStr (e.Parameter)
My.Settings.CollectionFeedUrl = url
My.Settings.Save()
Dim thread = New System.Threading.Thread(AddressOf FetchFeed)
thread.Start(url)
End Sub
フィードURLの保存後、RSSフィード取得処理に遷移します。上記コード内にあるFetchFeedメソッドはこの後作成します。
RSSフィードの解析
コレクションの取得でコレクションのURLが取得できました。この実体はRSSフィードのURLです。デバイスはRSSフィードを解析してユーザー所望の画像を表示します。それでは、RSSフィードを取得し、デバイスに必要な処理を順に実装していきます。
RSSフィード取得時の処理はFetchFeedというメソッドを作りその中に記述していくことにします。このメソッドと、この後必要となる記述を次のようにコードに追加してください。
Private WithEvents FeedTimer As New DispatcherTimer
Private WithEvents SlideShowTimer As New DispatcherTimer
Private CachedImages As New Dictionary(Of String , String )
Private CachedImagesLock As New ReaderWriterLockSlim
Private SlideShowImageUrls As New List(Of String )
Private SlideShowIndex As Integer
Private SlideShowImageUrlsLock As New ReaderWriterLockSlim
Private Sub FetchFeed(ByVal state As Object )
End Sub
RSSフィードの取得とそのタイミング
Live FrameItから配信される画像には天気予報やニュースなど時間経過とともに表示する価値が失われるもの、またユーザーが表示時間帯を設定したものなどが含まれているものがあります。デバイスはこれらを適切に表示しなければいけません。そのためにLive FrameItが発行するRSSフィードにはそのフィードの有効期限が含まれています。これを利用してデバイスは、適切なタイミングでRSSフィードを再取得します。
GetCollectionInfoメソッドでは次のようなRSSフィードのURLが得られます。
http://rss.frameit.com/GenRss/genrss.ashx?id=...&pin=...
※pinパラメータはユーザーが暗証番号(シークレットナンバー)を 設定していない場合ありません。
このURL先の内容を確認してみましょう。フィードの先頭部分は次のような構成になっています。
<? xml version = "1.0" encoding = "utf-8" ?>
< rss version = "2.0" xmlns:frameit = "http://www.frameit.live.com/firss/" xmlns:media = "http://search.yahoo.com/mrss/" >
< channel >
< ttl > 53 </ ttl >
< title > 旅行写真 </ title >
< link > http://frameit.live.com </ link >
< generator > http://frameit.live.com </ generator >
< lastBuildDate > Tue, 13 Oct 2009 08:05:15 -0700 </ lastBuildDate >
< pubDate > Tue, 13 Oct 2009 08:05:15 -0700 </ pubDate >
< description />
< item >
...
<ttl>と<pubDate>という項目がフィードに含まれていることがわかります。ttlはTime-to-Liveの略語で、フィードの有効期間を示す値(分: Minute)が設定されています。そして、pubDateはデバイスがフィードを要求した日時です。pubDateの日時にttl分を加算した日時が、フィードを再取得するタイミングとなります。
では、ここまでをコードで書きます。RSSフィードの解析はXMLとして処理してもいいですが、フィードを表すSyndicationFeedクラスが.NET Frameworkにはありますので、今回はこれを使用します。使用するにはプロジェクトにSystem.Web.Servicesの参照を追加する必要があります。
FeedTimer.Stop ()
SlideShowTimer.Stop ()
Dim url = CStr (state)
Dim feed As SyndicationFeed
Try
feed = SyndicationFeed.Load(Xml.XmlReader.Create(url))
Catch ex As Exception
FeedTimer.Interval = New TimeSpan(0 , 1 , 0 )
FeedTimer.Start()
SlideShowTimer.Start()
Exit Sub
End Try
Dim ttl = (From ext In feed.ElementExtensions _
Where ext.OuterName = "ttl" _
Select ext.GetObject(Of Integer )()).SingleOrDefault
Dim pubDate = (From ext In feed.ElementExtensions _
Where ext.OuterName = "pubDate" _
Select ext.GetObject(Of String )()).SingleOrDefault
Dim span = CDate (pubDate).AddMinutes(ttl).Subtract(Now)
FeedTimer.Interval = span
FeedTimer.Start()
フィード取得はタイマーを用いて定期的に取得するため、タイマー用の処理をいろいろと記述していますが、要となる部分は、SyndicationFeed.Loadメソッドによるフィードの取得と得られたfeedオブジェクから<ttl>と<pubDate>要素を参照しているところです。<ttl>、<pubDate>ともRSS 2.0の仕様に定義されたものですが、SyndicationFeedクラスでは対応するプロパティが定義されていないため上記コードのようにElementExtensionsプロパティを使用して参照しています。
画像のダウンロード
続いてフィードに含まれている複数の<item>要素をみていきましょう。ひとつの<item>要素は次のような構成になっています。
< item >
< frameit:sourceIcon > http://image.frameit.com/GenImage/source.ashx?sq=1&st=6&mkt=ja-jp </ frameit:sourceIcon >
< frameit:sourceCategory > ニュースと情報 </ frameit:sourceCategory >
< frameit:sourceName > 天気予報 </ frameit:sourceName >
< title > 東京都 東京 </ title >
< link > http://1.image.frameit.com/GenImage/Item.ashx?... </ link >
< category > 東京都 東京 </ category >
< guid isPermaLink = "true" > http://1.image.frameit.com/GenImage/Item.ashx?... </ guid >
< description >
<![CDATA[ < img src = "http://1.image.frameit.com/GenImage/Item.ashx?..." />< br /> 東京都 東京
]]>
</ description >
< pubDate > Tue, 13 Oct 2009 08:05:15 -0700 </ pubDate >
< enclosure type = "image/jpeg" url = "http://1.image.frameit.com/GenImage/Item.ashx?..." />
< media:content type = "image/jpeg" width = "480" height = "400" url = "http://1.image.frameit.com/GenImage/Item.ashx?..." />
< media:thumbnail width = "77" height = "64" url = "http://1.image.frameit.com/GenImage/Item.ashx?...&thumb=1" />
</ item >
例はLiveFrame Itで生成された天気予報の画像です。デバイスが参照すべき画像のURLが<link>や<guid>などの多数の要素に含まれています。ただし、Live FrameItは画像URLを<media:content>、<enclosure>、<description>要素のうちからデバイスが参照することを想定して配信していますので、この3要素の中からデバイスに併せて選択し使用します。
デバイスは、画像表示に関して以下の動作が求められます。
画像はダウンロードしてデバイスのストレージにキャッシュしておく
フィードに記載された順に画像を表示する
キャッシュ動作は、ネットワークが常に接続されている状態とは限りませんので接続している間にダウンロードし、ネットワークが切断されているときも画像表示を可能にします。また同じ画像を表示する場合にネットワーク接続回数を削減できます。
フィードに記載された順に表示する理由は、Live FrameItのWebサイト上でユーザーはコレクションの表示順についても設定しているためです。デバイス側で表示順を変更するべきではありません。
以上の動作に加えて、先ほどのフィード再取得を行います。もし再取得時にネットワークが接続されていない場合は、キャッシュ済みの画像を表示し、ネットワーク回復後にフィードを更新するようにします。フィード更新により、新しい画像はダウンロードし、フィードから消えた古い画像はキャッシュからも削除するようにしましょう。
フィードから画像URLを参照するコードは次のようになります。今回は<media:content>要素からURLを取得しています。
SlideShowImageUrlsLock.EnterWriteLock()
SlideShowImageUrls.Clear()
SlideShowImageUrlsLock.ExitWriteLock()
Dim newImages = New List(Of String )
For Each item In feed.Items
Dim imageUrl = (From ext In item.ElementExtensions _
Where ext.OuterName = "content" _
AndAlso ext.OuterNamespace = "http://search.yahoo.com/mrss/" _
Select ext.GetReader.GetAttribute("url" )).SingleOrDefault
SlideShowImageUrlsLock.EnterWriteLock()
SlideShowImageUrls.Add(imageUrl)
SlideShowImageUrlsLock.ExitWriteLock()
newImages.Add(imageUrl)
Next
取得したURLの順は記憶しておく必要があるので、コレクション(ここではList(Of String)クラスなどのこと)操作も含まれています。
続いて画像のダウンロード部分を以下に示します。2回目以降のフィード取得時の動作も考慮して画像の削除を行ってから、新しい画像のみダウンロードしています。
Dim deletedKeys = New List(Of String )
For Each k In CachedImages.Keys
If newImages.Contains(k) Then
Continue For
End If
Try
CachedImagesLock.EnterReadLock()
My.Computer.FileSystem.DeleteFile(CachedImages(k))
deletedKeys.Add(k)
Catch ex As Exception
Finally
CachedImagesLock.ExitReadLock()
End Try
Next
CachedImagesLock.EnterWriteLock()
For Each k In deletedKeys
CachedImages.Remove(k)
Next
CachedImagesLock.ExitWriteLock()
For Each u In newImages
CachedImagesLock.EnterReadLock()
Dim contains = CachedImages.ContainsKey(u)
CachedImagesLock.ExitReadLock()
If contains Then
Continue For
End If
Try
Dim client = New WebClient
Dim file = System.IO.Path.Combine(CachePath, Guid.NewGuid.ToString)
client.DownloadFile(u, file)
CachedImagesLock.EnterWriteLock()
CachedImages.Add(u, file)
CachedImagesLock.ExitWriteLock()
Catch ex As Exception
End Try
Next
以上が、FetchFeedメソッド内のコードです。このコードはタイマーのTickイベントで別スレッドとして実行するようにしておきます。また、アプリケーション起動時にも既にコレクションのURLがある場合は呼び出されるようにします。
Private Sub FeedTimer_Tick(ByVal sender As Object , ByVal e As System.EventArgs) Handles FeedTimer.Tick
FeedTimer.Stop ()
Dim thread = New System.Threading.Thread(AddressOf FetchFeed)
thread.Start(My.Settings.CollectionFeedUrl)
End Sub
Private CachePath As String = System.IO.Path.Combine(My.Application.Info.DirectoryPath, "cache" )
Private Sub FrameWindow_Loaded(ByVal sender As Object , ByVal e As System.Windows.RoutedEventArgs) Handles Me .Loaded
My.Computer.FileSystem.CreateDirectory(CachePath)
If My.Settings.DeviceId = Then
Exit Sub
End If
GetCollectionInfo()
If My.Settings.CollectionFeedUrl <> Then
Dim thread = New System.Threading.Thread(AddressOf FetchFeed)
thread.Start(My.Settings.CollectionFeedUrl)
End If
End Sub
画像の表示
最後にダウンロードした画像をウィンドウに表示する処理を書いて完了です。以下のようにスライドショー用のタイマーのTickイベントに処理を記述します。
Private Sub SlideShowTimer_Tick(ByVal sender As Object , ByVal e As System.EventArgs) Handles SlideShowTimer.Tick
SlideShowTimer.Stop ()
Dim url As String
Dim count As Integer
SlideShowImageUrlsLock.EnterReadLock()
url = SlideShowImageUrls(SlideShowIndex)
count = SlideShowImageUrls.Count
SlideShowImageUrlsLock.ExitReadLock()
Dim exists As Boolean
Dim file As String =
CachedImagesLock.EnterReadLock()
If CachedImages.ContainsKey(url) Then
file = CachedImages(url)
exists = System.IO.File.Exists(file)
Else
exists = False
End If
CachedImagesLock.ExitReadLock()
If exists Then
Try
PhotoImage.Source = New BitmapImage(New Uri(file))
Catch ex As Exception
End Try
End If
Dim prevIndex = Interlocked.Increment(SlideShowIndex)
If prevIndex >= count - 1 Then
Interlocked.Exchange(SlideShowIndex, 0 )
End If
SlideShowTimer.Interval = New TimeSpan(0 , 0 , 10 )
SlideShowTimer.Start()
End Sub
以上です。実際に実行して動作を確認してみましょう。Live FrameItサービス以外で提供された画像は既にインターネット上から削除されていたり、画像としてファイルを読み込みなかったりするかもしれません。そういった場合も考慮した実装が必要です。
おわりに
今回はここまでです。いかがでしたでしょうか。アプリケーションの動作のためコード量が少し多かったですが、Live FrameItサービスとのやりとり部分やフィードの解析自体はそう難しいものではなかったと思います。
作成した仮想フォトフレームアプリケーションのソースコードは、下記のリンクからダウンロードできます。参考にしてみてください。記事中に記載しきれなかった内容も含まれています。また、一部のコードは記述を変更しています。
次回もLive FrameItのSDKについてです。今回紹介しきれなかった内容を扱う予定です。