iPhoneアプリ Picasaを表示するデジタルフォトフレーム ni-frame(ニフレーム)をリリースしました。
2010 年 12月 22 日 水曜日 kosukepackager for iphoneを使ってiPhoneアプリを作りました。
Picasaウェブアルバムを表示するデジタルフォトフレームアプリです。
packager for iphoneがRetina displayに対応していないという点を逆手にとって買い替えで不要になったiPhone3GとかiPodTouch第二世代などデジタルフォトフレームに活用してはいかがでしょうか。
表示するPicasaのアカウントとログインするPicasaアカウントを設定できるので友人や家族があなたに共有しているアルバムを表示できるように出来ています。
9月に規約が変更されたとはいえ、packager for iphoneで制作ということでちゃんとリリースまでたどり着けるのか半信半疑でしたが(実際別の理由で一度リジェクトされましたので)無事リリースできて良かったです。
使ってみてわかったことですがpackager for iphoneはまだまだ未熟な部分があって、UIKitに限らず利用できないiPhoneのAPIが沢山あるということ。たとえばメールアプリに添付するとか、カメラロールを参照するとか、カメラを利用するとか、え、これも実装されていないの?っていう部分が多いです。
これは単純に未完なのかもしれないし、Appleとの関係で実現できなかったのかもしれないし、まったく別の理由なのかもしれない。というのも興味深いと思うことがあって、airの1ソースで複数プラットフォームに対応という理念がandroid向けとも思えるair2.5ではクロスプラットフォームの基本は守りつつも、それに執着するよりは特定プラットフォームに特徴的な機能には専用のAPIを用意するような方針になってきていると思えること。
もしそうなら開発を再開した(という)packager for iphoneは今後どうなるんだろう?
いずれにしても特にUIパーツは1から作るのがあまりに効率的でないことが多いので、次期Flex SDKで採用されるモバイルプロジェクト用コンポーネントやクラスライブラリがそのままの形でiPhoneでも使えるようになるのか、iPhone用にUIKitへアクセスできるAPIが実装されるかなったらいいなと思う。希望的には両方実装してくれと願うが。
Appleがクロスコンパイラを禁止した時の理由の一つにサイドパーティの開発環境に依存して最新のSDKの機能がなかなか浸透しないといった理由がありましたが実際に使ってみるとその憂いも理解出来ると思った。反面、次期Flex SDKのandroid向け機能の完成度を見ると、そんなにFlashを嫌っていなければpackager for iphoneはもっと完成された状態だったんじゃないかと思えちゃうのだよね。
Appleは今も決して積極的にpackager for iphoneのような開発環境を良く思っているように思えないし、今だFlash Playerすら許可される気配すらないわけですけどいい加減利用できるようにしてもいいんじゃないでしょうかね…。
そしてadobeももう一度本腰をいれてpackager for iphoneを開発してくれたらなと思う。
なんでかっていったらiPhoneとandroidを比べたらやっぱりプロダクトとしてはiPhoneが数段好きだ、だけども開発はActionScriptで作りたい。俺は。
TweetボタンとLikeボタン
2010 年 9月 15 日 水曜日 kosuke使うことになりそうなのでブログに設置。確認中。押してみるしかないし。
YouTube API 3
2010 年 5月 17 日 月曜日 kosukeGWにさわったYouTube APIはプレイヤーまわりだけでしたが、その後真打ち部分、YouTube Data APIを少し試してみました。
Data APIは、プレイヤーに渡す動画を選択したり、YouTubeが備えるコミュニティ機能を扱うAPIです。リファレンスはこちら。
Data APIではフィードを使ってやりとりをしていきます。リクエストの種類がいろいろあるので、必要な情報はおそらく得られると思いますが、思ったように使うのはやっぱり大変ですよね。
ひとまず、作ってみたサンプルがこちら。
動画の検索結果のサムネイルをロモウォール風に表示して動画を選べます。
忘れないうちに、つまずいたところを記憶しておきます。
動画の検索については、リファレンスの「動画の取得と検索」の「動画の検索」にある、
http://gdata.youtube.com/feeds/api/videos |
をリクエストします。
すると、「動画のフィードとエントリの説明」にあるような、検索結果のフィードが返ってくる。この検索結果の「entryタグ」がヒットした動画の情報になっています。
リクエストの際、取得する検索結果の位置や数を指定したりするパラメータがあるのだけれど、ここでまずころぶ。
取得する検索結果を数は、
max-results |
というパラメータで指定できる。(最大50件まで)
検索結果をどこから取得するかは、
start-index |
で指定できます。
検索にヒットした動画の数は、フィードの最初の方にある、
openSearch:totalResults |
タグでわかります。
ここまでざっと見て、ページング(ページ分け)をするには、次のフィードを取得する検索パラメータを、totalResultに至るまでstart-indexにmax-resultsを足した位置とすればよいと思いました。でも、これはNG。
openSearch:totalResultsの説明
を良く読むと、なんとtotalResultsの値は「おおよその値」を示していて、正確な値を示しているとは限らないとある。え?なんともあいまいなと思うのだが、実際検索してみると、頻繁にtotalResultsの数が上下するわけです。
したがってページ分けにこのopenSearch:totalResultsは使えない。
ページ分けをするには、totalResultsのリファレンスの下に書かれている<link rel=”next”> と <link rel=”prev”> を使って行います。
これ、はまった。。。。
次にプレイヤーなんですが、イベントの取得に、
player.addEventListener(event:String, listener:Function):Void |
を使いますが、これが消せないっぽい。
player.destroy():Void |
なんてそれっぽいAPIもダメでした。Loaderをunloadしてもnullしても消えなくて、何度も読み込む場合、付与したイベントが問題になったりする。新しいLoaderを作れば以前のリスナーが動く機会は無くなるけど、これだとメモリにどんどんゴミが溜まっていっちゃう。どうもそもそも消えない?っぽいので、プレイヤーは一度ロードしたらアンロードせず、cueVideoByUrlやloadVideoByUrlなどで新しい動画を読み込んだほうがよいかもしれません。
YouTube API 2
2010 年 5月 6 日 木曜日 kosuke前のエントリーの続き。クロムレスプレイヤーを試します。
クロムレスプレイヤーをロードするAPIは、プレイヤーがロードされた段階ではビデオは表示されないので、ロードされたらビデオを渡します。
VIDEO_IDで渡す
player.cueVideoById(videoId:String, startSeconds:Number, suggestedQuality:String):Void player.loadVideoById(videoId:String, startSeconds:Number, suggestedQuality:String):Void |
URLで渡す
player.cueVideoByUrl(videoId:String, startSeconds:Number, suggestedQuality:String):Void player.loadVideoByUrl(videoId:String, startSeconds:Number, suggestedQuality:String):Void |
このAPIの挙動が微妙に違っていてcueVideoById、cueVideoByUrlは自動的に再生しないがloadVideoById、loadVideoByUrlは自動的に再生される。
第2・第3引数は共通で、startSecondsは再生を開始する位置。suggestedQualityはビデオの解像度です。
ビデオを読み込み後は、FLVPlayerbackコンポーメントなんかと同様に、Player APIを使ってイベント処理や制御を組み込みます。
Player APIではプレイヤーの状態に応じて、
onStateChange
が発生します。
FLVPlaybackのように、再生ヘッドのインターバルだとか、バッファのインターバルのような、細かいイベントはないので、このonStateChangeを使って処理する形になりますが、肥大化しすぎのFLVPlaybackよりむしろ使いやすく感じました。
APIを組むのに補完が出ないのが面倒だったので、Proxyを作ってみた。
package video{ import flash.display.*; import flash.utils.*; public class YouTubePlayer extends Proxy{ public static const ON_READY:String = "onReady"; public static const ON_STATE_CHANGE:String = "onStateChange"; public static const ON_PLAYBACK_QUALITY_CHANGE:String = "onPlaybackQualityChange"; public static const ON_ERROR:String = "onError"; public function YouTubePlayer( object:DisplayObject ){ _object = object; } private var _object:DisplayObject; override flash_proxy function callProperty(methodName:*, ... args):* { var res:*; res = _object[methodName].apply(_object, args); return res; } override flash_proxy function getProperty(name:*):* { return _object[name]; } override flash_proxy function setProperty(name:*, value:*):void { _object[name] = value; } public function cueVideoById(videoId:String, startSeconds:Number, suggestedQuality:String):void{ flash_proxy::callProperty( "cueVideoById", videoId, startSeconds, suggestedQuality ); } public function loadVideoById(videoId:String, startSeconds:Number, suggestedQuality:String):void{ flash_proxy::callProperty( "loadVideoById", videoId, startSeconds, suggestedQuality ); } public function cueVideoByUrl(mediaContentUrl:String, startSeconds:Number, suggestedQuality:String):void{ flash_proxy::callProperty( "cueVideoByUrl", mediaContentUrl, startSeconds, suggestedQuality ); } public function loadVideoByUrl(mediaContentUrl:String, startSeconds:Number, suggestedQuality:String):void{ flash_proxy::callProperty( "loadVideoByUrl", mediaContentUrl, startSeconds, suggestedQuality ); } public function playVideo():void{ flash_proxy::callProperty( "playVideo" ); } public function pauseVideo():void{ flash_proxy::callProperty( "pauseVideo" ); } public function stopVideo():void{ flash_proxy::callProperty( "stopVideo" ); } public function seekTo(seconds:Number, allowSeekAhead:Boolean):void{ flash_proxy::callProperty( "seekTo", seconds, allowSeekAhead ); } public function mute():void{ flash_proxy::callProperty( "mute" ); } public function unMute():void{ flash_proxy::callProperty( "unMute" ); } public function isMuted():Boolean{ return flash_proxy::callProperty( "isMuted" ); } public function setVolume(volume:Number):void{ flash_proxy::callProperty( "setVolume", volume ); } public function getVolume():Number{ return flash_proxy::callProperty( "getVolume" ); } public function setSize(width:Number, height:Number):void{ flash_proxy::callProperty( "setSize", width, height ); } public function getVideoBytesLoaded():Number{ return flash_proxy::callProperty( "getVideoBytesLoaded" ); } public function getVideoBytesTotal():Number{ return flash_proxy::callProperty( "getVideoBytesTotal" ); } public function getVideoStartBytes():Number{ return flash_proxy::callProperty( "getVideoStartBytes" ); } public function getPlayerState():Number{ return flash_proxy::callProperty( "getPlayerState" ); } public function getCurrentTime():Number{ return flash_proxy::callProperty( "getCurrentTime" ); } public function getPlaybackQuality():String{ return flash_proxy::callProperty( "getPlaybackQuality" ); } public function setPlaybackQuality(suggestedQuality:String):void{ flash_proxy::callProperty( "setPlaybackQuality", suggestedQuality ); } public function getAvailableQualityLevels():Array{ return flash_proxy::callProperty( "getAvailableQualityLevels" ); } public function getDuration():Number{ return flash_proxy::callProperty( "getDuration" ); } public function getVideoUrl():String{ return flash_proxy::callProperty( "getVideoUrl" ); } public function getVideoEmbedCode():String{ return flash_proxy::callProperty( "getVideoEmbedCode" ); } public function addEventListener(event:String, listener:Function):void{ flash_proxy::callProperty( "addEventListener", event, listener ); } public function destroy():void{ flash_proxy::callProperty( "destroy" ); } } } |
最低限の操作系を用意。Play・Pause、Stop、シークバー。
今回はAPIの確認なんで作り込まず、Flexのコンポーネントで定義。
<?xml version="1.0" encoding="utf-8"?> <s:Group xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" width="400" height="300" creationComplete="init()"> <s:layout> <s:BasicLayout/> </s:layout> <fx:Script> <![CDATA[ private function init():void{ alpha = 0.5; seekBar.scaleY = 0.25; seekBar.decrementButton.visible = false; seekBar.incrementButton.visible = false; } ]]> </fx:Script> <fx:Declarations> <!-- 非ビジュアルエレメント (サービス、値オブジェクトなど) をここに配置 --> </fx:Declarations> <s:Button x="55" y="0" label="Pause" id="playPause" width="50" fontSize="10"/> <s:Button x="0" y="0" label="Stop" id="stop" width="50" fontSize="10"/> <s:HScrollBar x="94" y="8" width="320" id="seekBar"/> </s:Group> |
プレイヤーを格納する表示オブジェクト
package video{ import flash.display.*; import flash.events.*; import flash.net.*; import jp.progression.casts.*; import jp.progression.casts.mx.*; import jp.progression.commands.*; import jp.progression.commands.display.*; import jp.progression.commands.net.*; import mx.events.*; public class ChromelessPlayerContainer extends CastUIComponent{ public function ChromelessPlayerContainer(){ super(); _loader = new Loader(); _controller = new Controller(); _controller.y = 305; //トラックを操作した時のアニメーション、マウス位置に移動させる _controller.seekBar.setStyle( "smoothScrolling", false ); _controller.seekBar.setStyle( "repeatInterval", 0 ); _controller.seekBar.setStyle( "repeatDelay", 0 ); } private var _loader:Loader; //ローダー private var _player:YouTubePlayer; //プレイヤーオブジェクト private var _controller:Controller; //コントローラー protected override function atCastAdded():void{ addCommand( new LoadURL( new URLRequest( "http://gdata.youtube.com/feeds/api/videos" ), { onStart:function():void{ var vars:URLVariables = new URLVariables(); vars.vq = "teI8o6k67-k"; vars.format = 5; this.request.data =vars; } } ), new Func( function():void{ var _feed:XML = new XML( this.latestData ); //xmlが持つ名前空間を格納 var _media:Namespace = _feed.namespace( "media" ); //feedのyt名前空間 var _yt:Namespace = _feed.namespace( "yt" ); //feedのyt名前空間 _loader.contentLoaderInfo.addEventListener( Event.INIT, onInit ); _loader.load( new URLRequest( "http://www.youtube.com/apiplayer?version=3" ) ); function onInit( e:Event ):void{ e.target.removeEventListener( e.type, arguments.callee ); _player = new YouTubePlayer( _loader.content ); _player.addEventListener( YouTubePlayer.ON_STATE_CHANGE, onStateChange ); _player.addEventListener( "onReady", onReady ); } function onReady( e:Event ):void{ e.target.removeEventListener( e.type, arguments.callee ); _player.setSize( 400, 300 ); //プレイヤーに映像を表示する //表示するURLを取得 var q:QName = new QName( _media, "content" ); var media:XMLList = _feed.descendants( q ); //取得した要素のformatからswf形式を取得 for each( var node:XML in media ){ if( node.@_yt::format == 5 ){ break; } } //読み込み _player.loadVideoByUrl( node.@url, 0, "default" ); _loader.dispatchEvent( new Event( Event.COMPLETE ) ); } }, null, _loader, Event.COMPLETE ), new AddChild( this, _loader ), new AddChild( this, _controller ), new Func( function():void{ //停止ボタン _controller.stop.addEventListener( FlexEvent.BUTTON_DOWN, onMouseDownStop ); //再生、一時停止ボタン _controller.playPause.addEventListener( FlexEvent.BUTTON_DOWN, onMouseDownPlayPause ); //シーク操作 _controller.seekBar.addEventListener( FlexEvent.CHANGE_START, onChangeStart ); _controller.seekBar.addEventListener( FlexEvent.CHANGE_END, onChangeEnd ); }) ) } //停止ボタンを押したら private function onMouseDownStop( e:FlexEvent ):void{ //ビデオを停止 _player.stopVideo(); } //再生・一時停止ボタンを押したら private function onMouseDownPlayPause( e:FlexEvent ):void{ //ボタンにPlayが表示されていたら if( _controller.playPause.label == "Play" ){ //一時停止 _player.playVideo(); } //ボタンにPauseが表示されていたら else{ //再生 _player.pauseVideo(); } } //プレイヤーの状態が変わったら private function onStateChange( e:Event ):void{ var state:int = _player.getPlayerState(); //サムの進行を停止 _controller.removeEventListener( Event.ENTER_FRAME, onEnterFrame ); switch( state ){ case -1: //停止なら _controller.playPause.label = "Play"; _controller.seekBar.value = 0; break; case 0 : //最後まで再生したら break; case 1 : //再生なら _controller.playPause.label = "Pause"; //サムの進行開始 _controller.addEventListener( Event.ENTER_FRAME, onEnterFrame ); break; case 2 : //一時停止なら _controller.playPause.label = "Play"; break; case 3 : //バッファ中なら break; case 5 : //キューされたら //cueVideoById、cueVideoByUrlの時、発生 break; } } //ユーザーがシーク操作を開始したら private function onChangeStart( e:FlexEvent ):void{ _isSeek = true; } //ユーザーがシーク操作を完了したら private function onChangeEnd( e:FlexEvent ):void{ _isSeek = false; if( _player.getPlayerState() < 0 ){ _player.playVideo(); } _player.seekTo( ( _controller.seekBar.value / _controller.seekBar.maximum ) * _player.getDuration(), true ); } //コントローラ操作中はtrue private var _isSeek:Boolean = false; //サムを再生位置に進める private function onEnterFrame( e:Event ):void{ //コントローラ捜査中は終了 if( _isSeek ) return; //現在の再生時間 var time:Number = _player.getCurrentTime(); //ビデオの合計時間 var total:Number = _player.getDuration(); _controller.seekBar.value = ( time / total ) * 100; } } } |
YouTube API
2010 年 5月 1 日 土曜日 kosukeGWで嫁と子どもは帰省しちゃったし、仕事は片付いたしで、良いのか悪いのかあまりにまとまった時間があるので使ったことのないWeb APIを学習してみる。YouTube API。FlashからYouTube APIにアクセスしてFlash上で再生することにトライ。
リファレンスはこちらです。
YouTube ActionScript 3.0 Player API Reference
デフォルトの操作パネルをもつ「embedded player」と、ビデオエリアだけが表示される「chromeless player」とがある。独自の操作パネルを設定するなら「chromeless player」を使う。
それぞれのプレイヤーを呼び出すAPIは、
chromeless player
http://www.youtube.com/apiplayer?version=3 |
embedded player
http://www.youtube.com/v/VIDEO_ID?version=3 |
chromeless playerは、プレイヤーをロード後にムービーを指定するようで、embedded playerはロード時にムービーのVIDEO_IDを渡す。VIDEO_IDについて記載を見かけなかったのだけど、ブラウザからYouTubeを表示した時のURLのクエリv=〜の部分のようです。
ひとまずembedded playerを試してみた。
おわり…。
embedded playerをSWFに埋め込むだけなら、これだけっぽい。
ポイントになりそうなところは、ロードがINIT状態になったら、さらにコンテンツがonReadyイベントを返すまで待つこと。
このイベントをもってプレイヤーの初期化が完了しプレイヤーのAPIが利用できるようになる。
あとはリファレンスに載っているAPIを使ってプレイヤーを好きなように操作出来るはず。
拍子抜けするくらい簡単に使えるようだが、クロムレスプレイヤーとあわせてもう少しAPIを追ってみようと思う。
Twitterの投稿欄に入力するリクエスト
2010 年 2月 18 日 木曜日 kosukeTwitterを全然利用しないので(特にFlasherには前から流行っているのは知ってるけどニガテなんす。この手のもの)知らなかったのだが、TwitterにstatusパラメータをつけたURLでリクエストすると自分の投稿欄にstatusの値が入力されるってことを最近知りました。
こんな感じ。
http://twitter.com/home/?status=aiueo
Twitterのアカウントを持っていてログインしているなら、投稿欄に「aiueo」と入力されるはず。
何も考えずにFlashからこれを使ったら、
var req:URLRequest = new URLRequest( "http://twitter.com/home/?status=aiueo" ); navigateToURL( req ); |
これで同様に投稿欄に入力されているので、ヨシッと思ったのですが、
//2バイト文字を含む var req:URLRequest = new URLRequest( "http://twitter.com/home/?status=あいうえお" ); navigateToURL( req ); |
とか、
//メタ文字を含む var req:URLRequest = new URLRequest( "http://twitter.com/home/?status=http://nipx.jp/#/nipx" ); navigateToURL( req ); |
だとうまくいきません。
これはURLエンコードされないのが原因。端折らずちゃんとURLVariablesで値を渡せば解消されます。(問題の文字列をエンコードするって手もありますが。)
var req:URLRequest = new URLRequest( "http://twitter.com/home/" ); var vers:URLVariables = new URLVariables(); vers.status = "あいうえお/#/かきくけこ"; req.data = vers; navigateToURL( req ); |
さらにTwitterに送る文字コードはUTF-8である必要があります。
なので、FlashでSystem.useCodePage = true;を使っている場合、送信前にフラグをおろす必要があります。
//文字コードはUTF-8で送る必要あり System.useCodePage = true; //useCodePage = trueにしている場合 var req:URLRequest = new URLRequest( "http://twitter.com/home/" ); var vers:URLVariables = new URLVariables(); vers.status = "あいうえお/#/かきくけこ"; req.data = vers; System.useCodePage = false; navigateToURL( req ); System.useCodePage = true; |
Google Ajax Feed API で RSS READER(StarWas)を直す
2009 年 2月 25 日 水曜日 kosukeサーバー移転とWordPress化に伴い、「STAR WARS RSS READER」 が動きません。
自分ところのBlogで動けばいいって、勢いとノリで作ったのに、自分のところのBlogで動かなくなるとは思ってもいなかった。遠近文字もFlash10なら余裕だし、もういいかって感じですが、なるべく移行させるの原則のもと手を入れてみる。