Home > develop > ActionScript Archive

ActionScript Archive

IEのFlashPlayerはページを閉じてもrtmpセッションが切れないことがある

タイトルの現象は確認したものの、発生条件は確かなことはまだ言えないかも。。とりあえず現象報告とその対応策について。

問題

FlashPlayer上でRTMPセッションを張っているときにおいて、通常はページ遷移時においてFlashPlayerのインスタンスも同時に破棄される(はずな)ので、RTMPセッションもcloseされます。ところがIE7 + FlashPlayer9または10において、ページ遷移ではFlashPlayerが破棄されないケースがあるようです。なので、RTMPサーバ(たとえばWowza Media Server)にセッションを張っている場合、FlashPlayerが破棄されないことから、セッションが張りっぱなしになって、サーバ側でdisconnectをハンドリングできないことになります。

では、どうすれば破棄されるかというと、「IEを完全に終了」させて初めてFlashPlayerが破棄され、セッションが切れるようです。(ブラウザを閉じたときに初めてdisconnectイベントが上がってくることを確認しました。)Player側は、connectしてコンテンツを再生することについてはよく考えがちですが、ページのunload時においてはそもそも何も表示されていない状態なので、正常に接続がcloseされてあるかはちゃんと見ていないケースが多かったのが問題でした。

さて、このdisconnectイベントが上がってこない、というのは結構困るケースもあります。たとえばconnectイベントとdisconnectイベントが起きた時間を保存しておくと、サーバの任意の時刻におけるユーザの最大同時接続数を計測できますが、その計測はdisconnectイベントが上がってこないと難しくなります。(これはWebレイヤーだけだとちょっと難しいですね。)なので、この問題について、何らかの対策が必要となります。

対応策

対応策としては、こんな手順で対応できました。割と素直な方法。

  1. HTMLページのbeforeunloadイベントをフック
  2. RTMPセッションを張っているswfに対してExternalInterface経由でメソッド呼び出し
  3. swfは呼び出されたメソッドにおいて、NetConnectionをclose

と、こんなかんじです。beforeunloadのフックについてはこんなかんじでいいですね。swfはobject要素のid属性、およびembed要素のname属性が"externalpl"で指定されてあるものとします。ExternalInterfaceで呼び出すメソッド名がcloseAllになります。

	window.onbeforeunload = function(event){
		var swf = (document.all? window['externalpl'] : document['externalpl']) || null;
		if(swf && swf.closeAll) { swf.closeAll();} 
		document.getElementById('swf-container').innerHTML="";
	};

ちなみに、メソッドを呼び出した後はswfをロードしているHTML要素のswf-containerを空にして、FlashPlayerをunloadさせています。実際はこれまでに書いたとおり、IEにおいてはunloadされないのですが、それ以外の環境ではこの時点でunloadされるのでswfのファイルサイズが大きい場合、ページ遷移を行いやすくする利点があります。(最近知りました)

コールされるswf側はExternalInterface.addCallbackで実行されるメソッドを登録しておけばOKですね。

import flash.external.ExternalInterface;
...
ExternalInterface.addCallbak("closeAll", _closeAll);
...
function _closeAll():void{
      nc.close();
      nc = null;
} 

これでIEのFlashPlayerを利用している場合でも、RTMPサーバ側でdisconnectイベントをフックすることが可能になります。なかなか面倒なバッドノウハウでした。。

ちなみに

この現象を確認したのはWowza Media Server1.6.0です。FMSだとちゃんとイベントが上がってきたりするのかどうか、なんかも気になるところです。

Flash Player上でP2P通信ができるRTMFPについて

  • (2008.12.19 22:00追記) peer IDをnear IDに修正

Adobe Labs - Stratus Sample Application

先日のAdobe MAXでFlashの新しいプロトコルRTMFPを扱うことができるサービス「Stratus」について発表がありました。これは簡単に言うと、ブラウザで何もインストールすることなくP2Pを実現できる神がかったプロトコル(=RTMFP)と、RTMFPをサポートするサービス(=Stratus)、という位置づけです。上の写真は実際にStratusを介してRTMFPによる通信で僕の家とオフィス(夜中なので真っ暗ですね)をつないでいるものです。

これらについては、Adobe Labsでの次の文章が非常に分かりやすいです。

で、この文章があまりに分かりやすいものだったので自分用メモの意味もこめて、ざっと訳してみました。なお、冗長な表現の箇所は省略していたり、上記文章とは順番が前後している箇所もあるのでご注意ください。

RTMFP(Real-Time Media Flow Protocol )の特徴

RTMFPはFlash Player10, Adobe AIR1.5で利用可能なプロトコルです。RTMFPは、次の特徴があります。

  • UDPベースによる通信のため低遅延(RTMPはTCPベース)
  • サーバを介さないP2Pによるピア間での直接通信。
  • 優先度付きデータの利用(映像と音声を両方利用する場合、音声データの通信を優先的に低遅延で通信できる)

これらの特徴によって、リアルタイムの協調作業(たとえば映像チャット、ボイスチャットなど)を要するアプリケーションの開発にRTMFPは非常に適していると言えます。

一方で、RTMFPにはRTMPにある次の機能はありません。

  • Streaming media(多分ファイルストリーミング)
  • SharedObject
  • Remoting

つまり、RTMFPは音声と映像のリアルタイムコミュニケーションを行うことだけに特化したもの、ということにご注意ください。

RTMFPを利用するためには

RTMFPによる通信を行うためには、RTMFPをサポートしているサーバに接続する必要があります。たとえば先に挙げた「Adobe Stratus」があります。サーバはFMSも将来的にはサポートされる予定です。

StratusはFlash Player同士をつなぐ仲介役のようなものです。ピア(実際のクライアント、ここではFlash Player)間同士の接続を行うにあたって、その接続の最初にネゴシエーションを行う機能を提供します。また、上記の通りRTMFPの機能から省かれているので、StratusもStreaming media, SharedObject, などはサポートしていません。

Firewall

RTMFPはUDPベースなので、ピア間で直接通信を行うことを可能にします(*)。そのため企業内などで設置されてあるFirewallは外向きのUDP通信を許可する設定を行っておく必要があります。

どうしても全通信が許可できない場合は、mms.cfg(**)にTURN proxy(***)を設定することができます。

RTMFPTURNProxy=ip_address_or_hostname_of_TURN_proxy

(*)詳細な情報はドキュメント上ではボカされていますが、おそらくSTUN(UDP Hole Punching)を利用した通信を行うのでしょう。その上で、StratusはSTUNサーバの機能を持っていることが予測されます。このあたりのNAT超えの話は次の文章がわかりやすく説明されています。

(**)mms.cfgについての詳細はここでは触れませんが、別途詳しいドキュメントがあります。

(***) UDPで外向きに通信できない場合は途中で転送サーバを用意する、というわけですね。そういや昔TCPでP2P通信を行うときにTURNサーバを書いたのを思い出しました。。

Stratus service

Flash Playerは必ずAdobe Stratus Serviceを利用する必要があります。ここでは、「rtmfp://stratus.adobe.com」のようなアドレスを利用して接続を行います。Stratusサービスに接続をするアプリケーションを開発するためにはdeveloper keyが必要になります。developer keyはここから取得することができます。(要Adobe ID)

セキュリティ

通信にはAES128bit暗号を利用しています。また、鍵交換アルゴリズムにはDiffie-Hellmannを利用しています。

一方で、SSLによる強固な暗号化通信はサポートしていません。その代わりにセキュリティ強化のためにsecurity nonceを利用しています。(ピアの成り済まし防止)

Stratusへの接続

次のようなコードでStratusサービスへ接続を行います。これはActionScript3.0によるコードで、CS4やFlex Builder3.0.2などでFlash Player10,またはAIR1.5をターゲットに設定したときにビルド可能です。

private const StratusAddress:String = "rtmfp://stratus.adobe.com";
private const DeveloperKey:String = "your-developer-key";
private var netConnection:NetConnection;
 
netConnection = new NetConnection();
netConnection.addEventListener(NetStatusEvent.NET_STATUS,
netConnectionHandler);
netConnection.connect(StratusAddress + "/" + DeveloperKey);

正しいdeveloper keyが入力された場合、NetConnection.Connect.Successイベントが返ってきます。

また、接続完了後は256bitのユニークなpeer ID(NetConnection.nearID)が割り当てられます。他のピアがこのピアに接続するためには、IPアドレスなどではなく、このpeer IDnear IDを知る必要があります。peer IDnear IDを交換する方法についてはRTMFPとはまったく別の話です。たとえばXMPPサービスやシンプルな専用Webサービスを利用するなどして、別途交換する必要があります。

ピアとの通信

peer IDnear IDを交換したら、実際にピアとの通信を行います。この場合、「送信用」「受信用」の2つのNetStreamオブジェクトを用意することになります。

送信側の処理は次のようになります。

private var sendStream:NetStream;
 
sendStream = new NetStream(netConnection, NetStream.DIRECT_CONNECTIONS);
sendStream.addEventListener(NetStatusEvent.NET_STATUS,
netStreamHandler);
sendStream.publish("media");
sendStream.attachAudio(Microphone.getMicrophone());
sendStream.attachCamera(Camera.getCamera());

ここで注意すべき点として、実際に受信者が送信者をsubscribeするまでは送信者のデータは送出されることはない、という点です。つまり受信者の存在を無視して一方的にデータを送り続けることはできない、ということですね。

また、受信者側は次のような処理を行います。ここでは送信者のpeer IDnear IDを指定していることに注目しましょう。

private var recvStream:NetStream;
 
recvStream = new NetStream(netConnection, id_of_publishing_client);
recvStream.addEventListener(NetStatusEvent.NET_STATUS, netStreamHandler);
recvStream.play("media");

ピアからの接続の許可/拒否

送信者が受信者からの接続の許可、拒否を選択したい場合はonPeerConnectメソッドを実装したオブジェクトをNetStream#clinetに指定してあげると良いです。

var o:Object = new Object();
o.onPeerConnect = function(subscriberStream:NetStream):Boolean
{
   if (accept) 
   {
      return true; 
   }
   else
   {
      return false; 
   }
}
sendStream.client = o; 

ちなみに、送信者側ではNetStream.peerStreamsプロパティに、全subscriber(受信者リスト)の情報が配列で保持されています。

sendStream.send() 

のメソッドを利用すると全subscriberに対して同一データが送出されますが、特定のsubscriberに対してデータを送出することも可能です。その場合は次のような処理になります。

sendStream.peerStreams[i].send();

接続数

送信者が接続を行うことができる受信ピア数の最大値は、NetConnection.maxPeerConnections プロパティで設定されてあります。初期値は「8」です。

また、NetConnection.unconnectedPeerStreamsプロパティでは、実際にデータ送信を行うまでには至っていない受信ピアのNetStream配列がセットされてあります。ここからデータ送信が開始されると、NetStreamオブジェクトはNetStream.peerStreams配列に移ります。

RTMFPサンプル

ビデオ電話アプリケーションが用意されてあります。NAT下のピア同士での接続を行うと、よさそうです。それぞれが自分の名前を入力し、その後に通信を行いたいユーザの名前を入力すると自動的にpeer IDが交換され、互いに通信が開始されます。一番上の写真はそのときの様子です。

まとめ

RTMFPの通信のコードはP2Pの通信が行われてるとは全く思われないほど非常に綺麗に隠蔽されてあります。これを見ていて昔Javaで似たようなことをするためのコードを書いていたときは非常に苦労した記憶が蘇りました。いやぁ、あのときはほんと泥のような時代だった。。。

さて、RTMFPはメディアによるストリーミングはサポートしていない、とありましたが、これは早速何とかなりそうです。WindowsではMany Camという静止画や動画を仮想カメラデバイスとして扱えるソフトウェアがあります。これでラップすることでP2Pのファイルストリーミングは可能になりそうです。

また、このRTMFPを利用することでインターネットの歴史上、はじめて「(実質)インストールレスでブラウザ上でP2P通信を行うこと」が可能になりました。これはやばいです。相当画期的。実質、と書いたのはFlash Playerのプラグインだけインストールすることになるのですが、もはやFlash Playerがインストールされていないデスクトップ環境を探す方が難しいと思いますので「インストールレス」と言い切っていいと思います。これによって音声、映像ストリーミングと一緒に他のデータを流し込むことができればそのデータをJavaScriptで取り出してブラウザをごにょごにょして、、、と(ハック的にも)夢も広がりまくりですよね!!

そんなわけで駆け足でRTMFPについて書いてみましたが、相当簡単にアプリが構築できそうなので次は実際にコードを自分でも書いてみたいと思います。

Wowza Media ServerでSWFVerification(っぽいこと)をする方法

Kitasando.asで話題になったのがSWFVerification。これはストリーミングサーバが特定のswfからのアクセスしか許さない、という機能。FMS3は接続を行うswfをあらかじめ登録しておくことで、接続時にハッシュ値の整合性を確認し、1byteでも異なっている場合は接続を許可しない、という仕様だそうです。(どうやって整合性とってるのかは謎。typesterさんも言及してました。

で、そんな機能ってたしかWowzaにもあったはずだよなぁ。。と思って調べてみるとそれっぽい機能がありました。ただ、FMS3とは少し違う仕様で、「接続を許可するswfの設置サーバのドメインを登録できる」というもの。ハッシュ値照合までしないので、厳密にはVerificationではないものの、セキュリティ的には結構これで事足りる気はします。つまりローカルにswfを落としてそこから接続を試みるような野良swfは接続できない、ということですね。

実際、FMS3のSWFVerificationはデプロイが問題になるみたいです。と、いうのもクライアントのswfを書き出し直すたびにFMS3にデプロイしなおす必要があるので、この作業が煩雑になる問題があるみたいですね。それを考えるとWowzaの接続元ドメインのホワイトリスト制はセキュリティという観点では割といい機能なんじゃないのかな、とも思います。

設定方法

超簡単。Application.xmlの冒頭のConnections/AllowDomainsの箇所に、許可ドメインをカンマで区切って羅列します。こんなかんじ。

<Connections>
	<AutoAccept>true</AutoAccept>
	<AllowDomains>lab.katsuma.tv</AllowDomains>
</Connections>

こうすると同じswfでもlab.katsuma.tvに設置したもののみがこのアプリケーションに接続できて、他のドメイン(たとえば、blog.katsuma.tv)に設置されたswfは接続できない、というものです。

また、検証コードも書いてみました。手元のWowzaに「live」というアプリケーションを作ってみてください(rtmp://localhost/live に接続できる状態にする)。その後、上でかいたドメイン縛りの記述を付け加えてWowzaを再起動し、Firebugを開いた状態で次のURLにアクセスしてみてください。

前者は

start init
["init", "blog", "rtmp://localhost/live/"]
start connect
["blog", "NetConnection.Connect.Rejected"]
["blog", "NetConnection.Connect.Closed"]

こんなかんじのログが出て、後者は

start init
["lab", "init", "rtmp://localhost/live/"]
["lab", "NetConnection.Connect.Success"]

こんなログが出力されるかと思います。どちらも同じswfですが接続元から接続可能かどうかの結果が分かれています。

まとめ

SWFVerificationでWowzaの導入をためらっていた方はこの方法を検討してみる、というのも手かもしれません。デプロイも気にしなくていいから楽ですよ!

ソースコード

あと、いきなりローカルに接続されると怪しく思う人もいると思うのでソース晒しておきます。突貫コードですけど。ちなみにsecondlifeさんのlog関数を利用しています。

RTMPConnectionText.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
	paddingTop="0" paddingRight="0" paddingBottom="0" paddingLeft="0"
	creationComplete="init()">
	
	<mx:Script>
		<![CDATA[
			
			private var swfID:String;
			
			private function init() : void 
			{				

				log("start init");
				var server:String = Application.application.parameters.server || '';
				this.swfID = Application.application.parameters.id || '';
				log( this.swfID, "init", server);
			
				var nc : NetConnection= new NetConnection();
				nc.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
				nc.addEventListener(IOErrorEvent.IO_ERROR, ioErrorEventHandler);
				nc.connect(server);
			}
			
			private function netStatusHandler(evt:NetStatusEvent) : void
			{
				var code : String = evt.info.code;
				log(this.swfID, code);
			}
			
			private function ioErrorEventHandler(evt:IOErrorEvent) : void
			{
				log(this.swfID, evt);
			}
			
		]]>
	</mx:Script>
	
</mx:Application>

Application.xml

Wowza/conf/live/につっこんで再起動。

<Root>
	<Application>
		<!-- Uncomment to set application level timeout values
		<ApplicationTimeout>60000</ApplicationTimeout>
		<PingTimeout>12000</PingTimeout>
		<ValidationFrequency>8000</ValidationFrequency>
		<MaximumPendingWriteBytes>0</MaximumPendingWriteBytes>
		<MaximumSetBufferTime>60000</MaximumSetBufferTime>
		-->
		<Connections>
			<AutoAccept>true</AutoAccept>
			<AllowDomains>lab.katsuma.tv</AllowDomains>
		</Connections>
		<!--
			StorageDir path variables
			
			${com.wowza.wms.AppHome} - Application home directory
			${com.wowza.wms.ConfigHome} - Configuration home directory
			${com.wowza.wms.context.VHost} - Virtual host name
			${com.wowza.wms.context.VHostConfigHome} - Virtual host config directory

			${com.wowza.wms.context.Application} - Application name
			${com.wowza.wms.context.ApplicationInstance} - Application instance name
			
		-->
		<Streams>
			<StreamType>live-lowlatency</StreamType>
			<StorageDir>${com.wowza.wms.AppHome}/content</StorageDir>
			<Properties>
				<!-- Properties defined here will override any properties defined in conf/Streams.xml for any streams types loaded by this application -->
				<!--
				<Property>
					<Name></Name>
					<Value></Value>
				</Property>
				-->
			</Properties>
		</Streams>
		<SharedObjects>
			<StorageDir></StorageDir>
		</SharedObjects>
		<Client>
			<IdleFrequency>-1</IdleFrequency>
			<Access>
				<StreamReadAccess>*</StreamReadAccess>
				<StreamWriteAccess>*</StreamWriteAccess>
				<StreamAudioSampleAccess></StreamAudioSampleAccess>
				<StreamVideoSampleAccess></StreamVideoSampleAccess>
				<SharedObjectReadAccess>*</SharedObjectReadAccess>
				<SharedObjectWriteAccess>*</SharedObjectWriteAccess>
			</Access>
		</Client>
		<RTP>
			<!-- RTP/Authentication/Methods defined in Authentication.xml. Default setup includes; none, basic, digest -->
			<Authentication>
				<Method>digest</Method>
			</Authentication>
			<!-- RTP/AVSyncMethod. Valid values are: senderreport, systemclock, rtptimecode -->
			<AVSyncMethod>senderreport</AVSyncMethod>
			<MaxRTCPWaitTime>12000</MaxRTCPWaitTime>
			<Properties>
				<!-- Properties defined here will override any properties defined in conf/RTP.xml for any depacketizers loaded by this application -->
				<!--
				<Property>
					<Name></Name>
					<Value></Value>
				</Property>
				-->
			</Properties>
		</RTP>
		<MediaCaster>
			<Properties>
				<!-- Properties defined here will override any properties defined in conf/MediaCasters.xml for any MediaCasters loaded by this applications -->
				<!--
				<Property>
					<Name></Name>
					<Value></Value>
				</Property>
				-->
			</Properties>
		</MediaCaster>
		<MediaReader>
			<Properties>
				<!-- Properties defined here will override any properties defined in conf/MediaReaders.xml for any MediaReaders loaded by this applications -->
				<!--
				<Property>
					<Name></Name>
					<Value></Value>
				</Property>
				-->
			</Properties>
		</MediaReader>
		<!-- 
		<Repeater>
			<OriginURL></OriginURL>
		</Repeater> 
		-->
		<Modules>
			<Module>
				<Name>base</Name>
				<Description>Base</Description>
				<Class>com.wowza.wms.module.ModuleCore</Class>
			</Module>
			<Module>
				<Name>properties</Name>
				<Description>Properties</Description>
				<Class>com.wowza.wms.module.ModuleProperties</Class>
			</Module>
			<Module>
				<Name>logging</Name>
				<Description>Client Logging</Description>
				<Class>com.wowza.wms.module.ModuleClientLogging</Class>
			</Module>
			<Module>
				<Name>flvplayback</Name>
				<Description>FLVPlayback</Description>
				<Class>com.wowza.wms.module.ModuleFLVPlayback</Class>
			</Module> 
		</Modules>
		<Properties>
			<!-- Properties defined here will be added to the IApplication.getProperties() and IApplicationInstance.getProperties() collections -->
			<!--
			<Property>
				<Name></Name>
				<Value></Value>
			</Property>
			-->
		</Properties>
	</Application>
</Root>

Kitasando.asで発表してきました

秘密裏にこっそりmixiさんで行われたKitasando.asで発表してきました。お題が「Flash - Server 通信の世界」だったので、meeting24.tvをネタにしながらWowza Media Serverの導入から監視までの一通りをざっくり話してきました。以下、発表資料です。

全体的に割とニッチな話題ではあるものの、mixiさん、Kayacさん、ウチの会社のまわりの人たちで20人弱が集まって、内容もすごく勉強になりました。発表のメモはこんな感じ。

某サービスについて

  • Kunzoさん
  • 内容はここでは書けません><
  • (でもすごく面白かった!)

"kamaitachi" perl flash media server

  • typesterさん
  • PerlのFMS実装、kamaitachiについて
  • CPANでインストールできる
  • exapmlesの中に動くものはぜんぶはいってる
  • Kamaitachi::Service::xxxx
  • サービスをwithしてあげると有効になる
  • 録画もできる!

  • Sniffer::RTMP
  • RTMP専用のスニファ
  • 汎用的なものを利用しててもだめ
  • パケットの最初からでしかキャプチャできない
  • 途中からだとうまくいかない

kamaitachiのことは前回のShibuya.pmを見てなんとなく概要は知っていたのですが、RTMP専用のスニファを作られていたことは知りませんでした。というか、これすごく便利そう。。。

Red5の絶対ハマらないインストール方法

  • Kayacの方、名前失念。。。
  • Red5のantは使うと絶対にうまくいかない
  • MacにインストールしてそれをSCPでサーバに上げる
  • これ以外の方法はやめたほうがいい
  • サーバに上げたあとにant server とか実行しないように!

Red5はリビジョンごとに挙動がかなり変わるので最近使ってなかったのですが、インストールスクリプトまで挙動が変わっていたとは。。それにしてもインストーラ使ってできたバイナリをサーバに上げるって方法は意外に盲点。ant serverの下りはすごく笑ったw

効率よい柴犬の閲覧方法

直前までtakesakoメソッドで資料作ってたけど結局ほとんど破棄していたのが笑ったw

じゃんけんゲームで学ぶFlash と FMS(red5)のネットゲーム開発 #1?

  • 越川直人さん
  • Red5 0.8rc1を使ったゲーム
  • 最新のSharedObject#setDirtyメソッドにはバグがある
  • 強制更新が通知されない

やっぱRed5はまだまだ不具合多そうだなぁ、という印象。あと微妙な心理戦を演出してたのがちょっといいかんじ。

まとめ

今回はストリーミングサーバの通信話が多かったですけど、次回はJS-ASの通信の話とかも聞きたい+話せたら話したいかもです。ExternalInterfaceって謎の仕様がすごく多いからちゃんと知識を共有したいなぁ、と思う次第です。

ExternalInterfaceでは対象swfをonLoad以降にロードしてはダメ

何かとはまりやすいJavaScript-ActionScript連携のExternalInterface。以前にもこんなエントリ書いてました。

今回、また新たにハマりポイントがあったのでメモしておきます。

IEでswfの参照が取得できない(nullが返る)

通常、JSからASの関数をコールする場合は

<object width="320" height="240" align="middle" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" id="externalpl">
<param value="sameDomain" name="allowScriptAccess"/>
<param value="true" name="allowFullScreen"/>
<param value="/swf/hoge.swf" name="movie"/>
<param value="high" name="quality"/>
<param value="#111111" name="bgcolor"/>
<embed width="320" height="240" align="middle" 
pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash" 
allowscriptaccess="sameDomain" allowfullscreen="true" bgcolor="#111111" quality="high" 
src="/swf/hoge.swf" name="externalpl"/>
</object>

と、hoge.swfがある場合にJS側で

var player = document.all? window['externalpl'] : document['externalpl'];

と、すると

player.hello();

なんかが実行できます。ただし、あらかじめhoge.swf側で

ExternalInterface.addCallback('hello', this._hello);
function _hello() : void
{
  // hogehoge 
}

などとしておく必要があります。

今回のハマりは、このJSからASの関数を呼び出すときに、IEの時の場合のみASの関数が実行できない、詳しく言うとASの関数を実行するためのFlashの(関数)オブジェクト参照を取得できず、nullが返ってくるという問題がありました。逆にFirefox, Safariなどではちゃんとplayerの参照が取得できて(正常に参照が取得できると、関数オブジェクトが取得できます)、helloメソッドの実行もうまくいくわけです。

よかれと思ったことが仇に

今回、というか僕は常々FlashVarsで値を渡しやすくするために、swfのタグの書き出しはHTMLを直接書くのではなくて、ラップするJavaScriptの関数を作って、それ経由で書き出すようにしています。いわゆるSWFObjectを利用してもいいのですが、たかだかタグ書き出しのためだけにライブラリをロードするのも通信がバカバカしいので自作することが多いです。たとえば

	var param = {
		type : 'windows', 
		name : 'vista'
	};
	var o = new OS(param);
	o.start();

な感じでJSのOS#startメソッドを実行するとFlashVarsでtype=windows&name=vistaの値を渡してobject/embedタグが書き出される、のような感じです。

こうしておくと書き出し方法をdocument.writeのように愚直に書き出すこともできるし、swfのサイズが大きい場合にHTMLがロードし終わった後にinnerHTML差し替えによって遅延書き出し方法を取る事も出来ます。このように書き出し方法やそのタイミングを自分の好みで変えられる上に、後者のような方法だとページ全体の体感速度を上げることもできて非常に便利なのです。ただし、そこに罠があったのです。。

swfをonLoad以降に書き出すとマズい

今回のケースではjQueryを使って、DOMContentLoaded/onLoadのタイミングでswfを動的に挿入させるようにしていました。要するにこんなかんじ。

$(function(){
	var param = {
		type : 'windows', 
		name : 'vista'
	};
	var o = new OS(param, "container");
	o.start();  
});

知っている方も多いかと思いますが、jQueryでは関数を$()で囲む事でDOMContentLoaded(画像のロードを待たない、純粋なDOM Tree構築後のタイミング。ただしDOMContentLoadedをサポートしていない場合はonLoadのタイミング。)で処理を実行させることができます。このタイミングで指定ID要素(=上の例だと"#container")に対してinnerHTMLにobject/embed要素書き換えでswfをロードさせるようにしました。FirefoxやSafariはこれで何ごともなく動くのですが、IEだと動作しません。

では、どうすればいいか?というと、object/embed要素の書き出し方法をbody.onload時ではなく、単純にXHTML上にdocument.writeでobject/embed要素書き出しに修正すると動きました。

どうやら

この結果から推測するに「IEはbody.onload時(DOMContentLoaded時)において存在しないswfの参照は取得することができない」ということが言えそうです。つまり、ExternalInterfaceを利用したい場合は、対象swfはDOMContentLoaded以降にロードされるのではなく、DOMContentLoaded以前にロードされておく必要がある、ということですね。もっと言うと、IEでwindow['swf_id']で参照するためのDOM Treeの参照(またはその複製?)がDOMContentLoaded以降に更新されていないように思われます。

知っておけばハマらないのですが気づかなければなかなか抜け出せない罠でした。しっかしExternalInterfaceの罠の多さはほんと異常です。。それもIEでハマることがやたら多い印象。このあたり、IE8になると少しは解消されたりするんでしょうか??

「FlashのCamera・Microphoneクラスを使うときの注意点」の注意点

Camera/Microphone security panel

trick7さんで「FlashのCamera・Microphoneクラスを使うときの注意点」というエントリがあがっていました。FlashでCamera, Microphoneを使うときに次の注意点が必要、とあります。

FlashでWebカメラやマイクを使う時の落とし穴。おなじみの上のパネルはFlashのステージサイズが横214px、縦156px以上ないと、どれだけスクリプトが正しくっても表示されない。Webカメラを使った横幅160pxのブログパーツを作るぞと意気込んで4時間もハマってしまったので、これは共有しておくべきだと思いました。ブログパーツを作ろうとしてる人は気を付けてくだせい。

ただ、これって8割ほど正解なのですが、ものすごく細かく言うと何が何でもカメラ、マイクを利用するときに214x156以上のステージサイズが要求されるかというと、そうでも無いのです。

上記のパネルが表示されるときはプライバシー保護のためなので、カメラやマイクで取得した映像、音声がFlashPlayerを通して表示、通信されようとするとき、つまり以下のattach系のメソッドを実行しようとしたときに表示されます。(もしかするとSound系でまだあったかも。。?把握しているのはこのあたりのメソッド)

  • Video#attachCamera
  • NetStream#attachCamera
  • NetStream#attachAudio

要するに実際に映像と音声を扱おうとしたときのみにセキュリティパネルが表示されるわけです。

では、それ以外の場合なんてありえるの?という話になるわけですが、たとえばCameraクラスには利用できるデバイスのリストを取得するCamera.namesというstaticなプロパティがありますが、このgetterではセキュリティパネルは表示されません。

var devices:Array = Camera.names;

たとえばカメラやマイクを扱うFlashアプリケーションで、ユーザの環境チェックを行いたいときに簡易的なセルフチェックページを作るなんかに、1x1のswfを作っておいてExternalInterface経由でJavaScriptからデバイス数を取得、なんてのも可能です。

とは言っても、こういうケースは実際には少なくほとんどのケースではCamera/Microphoneは何かしろのオブジェクトにattachするケースばかりでしょう。なので、やっぱりtrick7さんのおっしゃる通りにステージサイズには注意をしておいたほうがベターだと思います。

Live100でQikの動画を取り込み

Private Betaでこっそりやり続けてる、いろんなライブストリーミングのアグリゲーションサービスなLive100ですが、いままで取り込んでいたUSTREAM.TVとJustin.tvに加えて「iPhone 3Gからのライブ配信をサポート!(と、いってもAppStoreには並んでいない野良アプリだけど)」で噂のQik.comのストリーミングも取り込んで表示できるように本日なりました。

Live100 - aggregates Ustream, Justin and Qik

こんな感じ。拡大してる映像がQikのものです。

さすがにQikは携帯からの配信というだけあって、町中からの映像だったり部屋を歩き回ったような映像が多くて、おもしろい。自分を定点で撮影するのが多いUstream, サッカーの映像がやたら多いJustinたちと比べてみると、あまりにも対照的なゆるい映像がぽつぽつ出てくるのは結構シュール。

で、試してみたい人はここにメール送ってもらえれば、引き続きアカウント発行しますのでよろしくです。ぼーっとつけっぱなしにしながら、不意に出てくる綺麗な女子を見つけてニヤニヤ眺めておくライブ版4Uとしてもいいものです。

そろそろShibuya.asについて語っておこうか

少しの間、仕事の忙しさに任せて放置しちゃってたのですが、いい加減言い出しっぺが何かやらないと。。。と、いうわけで週末に専用のBlogを立てました。

何やらアニメソングの方の動向もここへきて動いているようなので、as-usersもじっとしてはいけませんね><

とりあえず僕としては、Shibuya.JSのような感じでActionScript(と、ECMAScriptも?)まわりの話で、ホットな技術動向や独自+おすすめライブラリの紹介だったり、勉強会的な場を渋谷界隈で作ることができたらなぁ、と思っています。as-users.jpSpark projectはどうなるの?なんて話も出てきそうですが、前者はいろんなネタ、情報のハブサイトであり、後者は実際のソースコードのハブサイト、Shibuya.asはそれらの中間に属するリアルな場の提供、なーんてことができたらな、、と実は最近ASあまり書かずにCakePHPばっかりいじってる人が思っていたりします(笑)

でも、ごはんとFlashでASerもやっとこさJSerに続いてがんがん増えつつあるのは感じ取れたので、その流れにのっていいものができたらな、というのはホントに思っています。なので、「手伝ってあげてもいいよ><」とか「こういう風に進めていけばいいと思う><」なんて意見を募集しています。意見がある方はこのエントリのコメントかSBM経由でのコメントをよろしくお願いします。

Utagoe Live100のデモ映像をアップロード

Mashableに載ったよー!な話をしても、実際どんなものかがわかりにくかったら伝わらないだろうと思ったので、利用している様子をLilypadでキャプチャしてVimeoYoutubeに上げてみました。ここではVimeoの方をembedしてます。


Utagoe Live100 Demo Movie from katsuma on Vimeo.

前のエントリーにも書いたけど、多くの映像を見やすくするために、D&Dで位置を自由に移動させたり、表示列数を簡単に変更できたり、マウスホイールでスケールサイズを簡単に変更できたり、GoogleMapと同じような感じな操作感が特徴的なのです。映像ではこのあたりの雰囲気が伝われば、と思います。あと、アカウント欲しい方は引き続きこちらからメールアドレスをお願いします。

ところで

Vimeoは今回はじめてUploadで使ってみたけどインターフェースめちゃかっこいいですね。uploadしてから見れるまでの時間も短いし、フォームのデザインの細かなところまで気を配ってて、いろいろ統合的に非常に好印象!

vimeoって美大っぽい、YouTubeは情報工学生のイメージ
- via Twitter/KEMONO

なんて話がひっかかってたんだけど、自分の中ですっと収まった気がします。まだまだ感覚的なんですけど。

ごはんとFlash に参加してきました

「Flash大好きな人がたくさん集まって、おいしいご飯を食べるイベント」な素晴らしい企画「ごはんとFlash」に参加してきました。(ネーミングほんとにいいですねー)

gohan to flash @ bowls

ActionScript系のこんなイベントは初めて参加したので、アウェイ感むんむんしてたらどうしよう。。とか思っていたのですが、実際はそんなことは全くなく、皆さん優しい方ばかりで非常に楽しい時間が過ごせました。特にTwitterやBlogでこちらから一方的に知っている大勢の方々と話せたのがよかったです。ll_koba_llさんがアイコンからグラップラー刃牙の中の人みたいな姿を勝手に想像してたら実際はぜんぜん違ってすごく優しそうな方だった、というギャップがツボだったりしてたのはここだけの話ですよ。。

gohan to flash @ bowls

皆さんの様子

ところで印象的だったのが、一昔前の「mixiとかやられてたりします?」な質問が完全に「Twitterやってます?」に置き換えられてる点。互いのIDを教え合うような姿もあちこちで見られました。あと、全体的にASな人よりも、デザイナ、タイムライン派の人たちの方が割合としては大きかった模様。「正直アウェイを感じる!」みたいな話をグニャラさん(て呼び方でいいのかな?)と話してたりしてました。来てる人たちの雰囲気もなーんとなくJSerの集まりとも少し違うのは印象的でしたね。

そしてShibuya.asの話も少し振ってみたりしてみたのですが、予想通りかなり笑われつつも、ありがたいこと&&意外にも反応もよさそうな感じで。ちょっと引くに引けなくなりつつある流れを感じたりしているので、これについてはまた別途エントリを書きたいと思います。

gohan to flash @ bowls

最後に

準備にかなり大変だったであろうkayacの皆さん、trick7寺井さん、FICC福岡さん、おいしいごはんを出してくれたbowlsの皆さん、協賛のメディアテクノロジーラボさん、素晴らしいイベントを開催いただいてありがとうございました!第二回も開かれたらぜひ参加してみたいと思います!

Shibuya.as

勢いでドメイン取ってしまった。

http://shibuyaas.org/

以下、取得までのいきさつです。

  • 最近仕事でめっきりASばっかり書いてる
  • どうもShibuya.asの「as」が「Anime Song」の略という噂を聞いたので、なんかそれはちょっと悔しい><
  • kambaraさんとたまたまお話させていただいたときに「ASもボチボチきてますよねー」「ですよねーですよねー」みたいな話をさせていただいた
  • Flash CS3とか使わないFlex SDKでゴリゴリとASerな人たちの話をもう少し聞いてみたい、GCやチューニングテクとか
  • 某Flash系のセミナーに出てみたけどなんとなく怖そうな人が多かったのを思い出した
  • 調べたらドメイン空いてた

とは言っても、今の時点で何か構想があるわけでもないです。でも、せっかくなんで何かやってみたいなぁ、なんて妄想もあります。

意図したNetStatusEventが上がってこないことへの対策

Cameraオブジェクトを利用して、RTMPでライブストリーミングを行うときに、ストリーミングの開始、終了をハンドリングするために肝なのがNetStatusEvent。NetStreamに対してハンドラを設定しておき、開始、終了を検知します。

ns.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
...
function netStatusHandler(evt:NetStatusEvent) : void {
	if(evt.info.code == "NetStream.Play.Start"){
		// 配信開始 				
	} else if(evt.info.code == "NetStream.Play.Reset"){ 
		// 配信中	
	} else if(evt.info.code == "NetStream.Play.UnpublishNotify"){
		// 配信停止
	} else {
		// その他
	}
}

基本的にはこんな感じ。

罠が多い

ところが、あくまでこの情報は「基本的」なもので、結構アテにならないことが多いです。NetStreamで受信しているストリームが1つであればまだイベントの発生も安定していますが、複数ストリームを同時に受信している場合、たとえば終了イベントがうまく起こらず、うまく終了処理が行えないことがあります。

このような場合、映像を表示しているVideoオブジェクトのclear()を呼び出すことができず、残像が残っちゃったようなことになってしまう、格好悪い表示になってしまうことがあります。今回はこんなとき、どうやってハンドリングすればいいか、という話。

Timerを走らせる

じゃあどうすればいいか、というと本当にライブ配信をしているかどうか、x秒ごとに定期的に映像のFPS(=フレームレート)をチェックするTimerを走らせるといい結果になります。一定時間ごとに数回確認して、n回連続して同じ低レートの値であれば、すでに表示しているユーザは配信を停止している、という発想。

ここでのポイントは「n回連続して同じ低レートの値であれば」という点。終了検知なら、FPS=0でハンドリングしてもよさそうですが、配信を停止していてもFPSは0になるとは限らず、0.1, 0.2くらいの値が出ることは割と多いです。なので、「0」ではなく、「低レートの値」がポイント。

また、配信対象物が停止している場合(風景とか)、これもかなり低い値が出ます。ただ、カメラで実際に映像を取得しているかぎり、毎秒常に同じ値が出ることはまず無いので、「n回連続して」というのが効いてきます。

実際に、どんな値であればいいのか、というと何度か試行錯誤した結果、こんなパラメータを僕は利用しています。

  • x = 15
  • FPS = 0.2
  • n = 3

つまり、「15秒ごとにFPSを調べて0.2以下の同じ値が3回連続して続いたら、そのユーザは配信を停止していると見なす」という意味ですね。

NetStream.Play.Resetは実は重要

あと、普段はほとんど利用しないであろうNetStatusEvent#info.code値のNetStream.Play.Resetですが、実は結構重要です。と、いうのも、ストリーミングを行っている中で、このイベントは割と頻繁に起こります。Livedocsによると「再生リストのリセットが原因です。」と、ありますが意味はよくわかりません。よく分かりませんが頻繁に発生します。頻繁に発生する、ということは「このイベントが発生しているかぎりユーザは停止せずに再生しつづけている」ということを示すものとして利用できます。

具体的には、上で示したFPS確認ロジックにおいて、NetStatusEvent#info.code==NetStream.Play.Resetであれば、確認カウンタをクリアさせます。(たとえFPSが低い値を出していても、ユーザは確実に再生を続けているから)。これらをまとめて、こんなTimerを用意しておけばOKです。

PlayerManager.as(的なもの)

var timer:Timer = new Timer(15000);
timer.addEventListener(TimerEvent.TIMER, streamCheckerHandler);
timer.start();
...

function streamCheckHandler(evt:TimerEvent) : void {
	for(var i:int = 0; i<players.length; i++){
		var player : Player = players[i];
		var ns : NetStream = player.getNetStream();
		var fps : Number = ns.currentFPS;
		if(fps < 0.2) {
			player.checkFPSHistory(fps);
		} 
	} 
}

Player.as(的なもの)

var prevFPS : Number = 0;
var fpsHistoryCount : int = 0;
const FPS_HISTORY_MAX_COUNT : int = 3;
...
function checkFPSHistory(fps : Number) : void {
	if(fps!=prevFPS){
		prevFPS = fps;
		fpsHistoryCount = 0;
	} else {
		fpsHistoryCount++;
	}			
	if(fpsHistoryCount==FPS_HISTORY_MAX_COUNT){
		unPublish();
		prevFPS = 0;
		fpsHistoryCount = 0;
	}
}
...
ns.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
...
fuction netStatusHandler(evt : NetStatusEvent) : void {
	if(evt.info.code == "NetStream.Play.Start"){
		// start to publish		
	} else if(evt.info.code == "NetStream.Play.Reset"){ 
		fpsHistoryCount = 0;
	} else if(evt.info.code == "NetStream.Play.UnpublishNotify"){
		stopPublish();
	} 
}

PlayerManagerがいくつかのPlayerオブジェクトを作成、参照をもってTimerを実行しているオブジェクトです。PlayerはVideoオブジェクトやNetStreamオブジェクトなどをメンバに持ち、実際に映像の再生を行うもの。こんな感じで全体の流れを掴んでいただければと思います。

FlashPlayer9のガベージコレクタのメモリ解放の考察

Tweenerのメモリ解放の挙動について調べている中で、そもそもFlashPlayer9(AVM2)のガベージコレクタの挙動が気になったので、簡単な検証コードを書いてみました。結構興味深い挙動をしていたことに気づいたのでメモ。ちなみにPlayerはWindowsデバッグ用プレイヤーを利用しています。これはガベージコレクタを強制的に呼び出すSystem.gc()がデバッグ用プレイヤーしか利用できないためです。

検証

検証用コードはこんなかんじ。Spriteオブジェクト作ってaddChildして、removeしてnullつっこんで、それぞれの過程でメモリ使用量を計測してるだけ。

package {
	import flash.display.Sprite;
	import flash.display.DisplayObject;
	import flash.events.Event;
	import flash.system.System;

	public class GCTest extends Sprite{
		
		public function GCTest() : void {
			log("[0]" + System.totalMemory);

			var sprite : Sprite = new Sprite();	
			log("[1]" + System.totalMemory);
			
			addChild(sprite);
			log("[2]" + System.totalMemory);
			
			removeChild(sprite);
			log("[3]" + System.totalMemory);
			
			System.gc(); // A
			log("[4]" + System.totalMemory);
			
			sprite = null;
			log("[5]" + System.totalMemory);
			
			System.gc(); // B
			log("[6]" + System.totalMemory);				
		}
	}
}

さて、単純に考えるとオブジェクト作ったものを完全に解放しきっているので[0]の値と[6]の使用量は同じ値になるはず。でも僕の環境での結果はこんな感じでした。

[0]2240512
[1]2330624
[2]2334720
[3]2338816
[4]2334720
[5]2347008
[6]2334720

オブジェクトの作成、addChildまでは分かりやすく使用量は上がっていますが、

  • GC実行直後はメモリ使用量が減らずに増える
  • nullを代入してオブジェクトをGC回収対象にしても完全にメモリ解放されていない([0 ]!=[5 or 6])

なんてことがわかります。とりあえずGC強制呼び出しは悪影響っぽいのでGC呼び出しをやめて、上の例のA, Bの箇所をコメントアウトするとこんな結果になりました。

[0]2240512
[1]2330624
[2]2334720
[3]2338816
[4]2338816
[5]2342912
[6]2342912

null入れたらメモリ使用量上がってます。なにこれw

見直し

そこでremoveChildの定義を見直し。するとこんな風に書かれています。

DisplayObjectContainer インスタンスの子リストから指定の child DisplayObject インスタンスを削除します。削除された子の parent プロパティは null に設定されます。その子に対する参照が存在しない場合、そのオブジェクトはガベージコレクションによって収集されます。DisplayObjectContainer の子より上位にある表示オブジェクトのインデックス位置は 1 つ下がります。 DisplayObjectContainer

なるほどー。removeChildすると、もうそれだけでGCの対象になるわけですね。これremoveChildだけじゃだめで、null参照させないとダメだと勘違いしてました。あと、Twitter経由でこんなこと教えていただきました。

@ryo_katsuma add 分と new 分というニュアンスがちとわからないのだけど、 GC はゴミが残るので正確な差分は求められないかもしれません
via Twitter

@ryo_katsuma オブジェクト単位じゃなくてブロック単位で開放するらしいので。と、 kamijo さんのブログに書いてありました。
via Twitter

kamijoさんのblogの該当記事がまだ見つけられていないのですが、どうもGCは逐一オブジェクト単位でメモリ解放を実行しているのではない、ということのようですね。また、null参照させたりして、解放の対象にさせても完全にクリーンアップしてくれることを保証してくれるわけでは無さそうです。

そんなわけで

GCの挙動を追っかけるためには、仕様をもう少し読みこなさないとダメぽいです。あとJavaのGCについてももう少し調べたいと思いました。

ActionScript3だけで作るUIコンポーネント

前回のエントリに対して、Technorati経由でAS3のスライダー実装のコードを別の切り口から書いてくれてる方を発見。(ありがとうございます!)

[ActionScript3]スライダーバーでのっかり

AS3に関しては我流度が相当高いので他の人が書いたコードは相当参考になります。しかも

細かいんですが、MOUSE_MOVEとMOUSE_UPがそのオブジェクト上じゃないとダメっつーのが嫌でStageオブジェクトに逐一 addEventListener。そもそもMOUSE_DOWN出来てる時点で表示リストに追加されているという算段。実はこれ、 FlashPlayerの描画領域を超えて、さらにはブラウザのウィンドウ領域を超えてマウスイベントが拾えるすぐれものtips。StageオブジェクトのMOUSE_MOVEとMOUSE_UPはStageからのドラッグだとイベントを送出するらしいです。

こんなの全然知らなかった!!FlashPlayerの外のイベントまで拾えちゃうのかぁ。。なんかいろいろ新しくできそうなことを模索しちゃいますね。こんな情報まで教えてもらえたので晒した甲斐があったなぁ。

基本コンポーネント作るのめんどくさい

上記のエントリ内でも言及されてますが、AS3でMXMLコンポーネント使えない(ですよね?)のがそもそも開発者を面倒がらせることになっているわけで。「UI含めて大部分の開発をAS3だけで行うんだけども、ボタンやスライダ、テキストエリアみたいな基本的なUIコンポーネントはMXMLのを使い回したい」なんて需要は相当あるんじゃないのかなぁと思っています。

MXMLが微妙にイケてないのが、まさにここの点であって、MXML文書内ではAS3のコードが書けるのに、AS3のコード内ではMXML要素を動的生成することができない(はず、少なくとも自分の調べによると)点。WebページにおけるHTMLとJavaScriptの関係がswfにおけるMXMLとActionScriptであるなら、MXML要素の動的生成くらいサポートしてもよさそうなのに、、と思うのですが、この考えは間違っているのでしょうかね?

あと、CS3から追加されたfl.*パッケージについてもmxmlcがコンパイルできないのも微妙な点。コンパイルが通るようにライブラリをswcにまとめてくれてる人もいるけど、コンパイル通っても実際には表示されるべきUIコンポーネントで使われている画像が一切表示されないから、この話の流れ上では使えない、と言い切ってもよさそう。要するにUI系コンポーネント使えたけりゃCS3買えって話?

情報がまとまってほしい

こんな風に自作コンポーネント作ってる人は結構いると思うんだけども、情報がまとまっててほしい。codereposみたいに共有の場所にみんなでぽんぽん上げていくような。今度スクロールバーとか必要になったら「これはありそうだよなーさすがに」とか呟きながらググりまくる自分の姿が想像できるわけですよ。SBMに共通タグつけるのでもいいけど、何かしろの共有ルールが欲しいな、と。

ActionScript3だけでSliderを作る

スライダーっていうのはボリュームコントロールとかでよく使われてるアレ。クリックとドラッグイベントをとって値を変化させることができるやつ。mxmlで書けばスライダーなんて数行で作れちゃうのですが、ちょっと凝ったインターフェースを作る場合、mxmlでは納得できなかったりする場合があります。

で、バーやノブの部分を好き勝手な画像を設定させることができる汎用的なスライダーを作ってみました。D&Dのところでやや不自然な挙動をする場合もあるのですが、とりあえず満足できるものができたので制作過程含めて晒しておきます。スライダーに限らず、独自UIを作る場合に少しでも役立てばと思います。

カスタムイベントはそんなに難しくない

イメージとしては、

  1. flash.events.Eventを継承したカスタムイベントクラスを作成
  2. スライダーのUIを司るクラスがMOUSE_DOWN(バーをクリック)、またはMOUSE_DOWN/MOUSE_UP(ノブをD&D)した場合に、カスタムイベントを発生
  3. addEventListener(カスタムイベント, イベントハンドラ)でイベントを取得、処理

みたいな感じ。2,のカスタムイベントを発生させる場合、AS3ではflash.events.EventDispatcher#dispatchEvent()を使うとイベントの発生を行わさせることができます。このEventDispatchクラスはSpriteが実は継承しているので、UIを作るクラスは特に意識することばく、dispatchEventメソッドは利用できるはずです。なので、UI作成クラスのマウスイベントのハンドラ内でdispatchEventを実行すればOK。

で、とりあえずソースはこんな感じ。

SliderEvent.as

カスタムイベントのクラスです。イベントの名前を定義したら親クラスに処理を丸投げしてるだけ。プロパティvalueにSliderの値が入ります。

package {
	import flash.events.Event;

	public class SliderEvent extends Event {
		
		public static const CHANGE : String = 'change';
		public var value:int = 0;
		
		/*
		 * constructor
		 */
		public function SliderEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false) : void {
			super(type, bubbles, cancelable);
		}
		
		public override function clone():Event {
			return new SliderEvent(type, bubbles, cancelable);
		}
	}
}

Slider.as

UIのメインクラスです。bar, nobはそれぞれpng使ってます。スライダの取りうる値はコンストラクタで最小値、最大値、初期値を決めることができるようにしました。ノブをドラッグできる範囲をRectangleクラスを利用して決定しています。これをちゃんと決定させておかないと、y軸方向にもブレて気持ち悪い挙動になってしまいます。イベントハンドラはもうちと奇麗に書けそう。。このあたり、まだ慣れないです。setterを巧みに使って奇麗に書けないかなぁ。

package {
	import flash.display.Sprite;
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.events.EventDispatcher;
	import flash.geom.Rectangle;

	public class Slider extends Sprite {
		[Embed(source="./images/slider_bar.png")]
		private const imgBar : Class;
		private var bar : Sprite;
		
		[Embed(source="./images/slider_nob.png")]
		private const imgNob : Class;
		private var nob : Sprite;
		
		private var dragging : Boolean = false;
		private var dragArea : Rectangle;
				
		public var _value : Number;
		public var minimum : Number;
		public var maximum : Number;
		
		public static const CHANGE : String = 'change';
		
		/*
		 * constructor
		 */
		public function Slider( _min:Number=0, _max:Number=100, _val:Number=50) : void {
			this.minimum = _min;
			this.maximum = _max;
			this.value = _val;	
			
			// background
			this.bar = new Sprite();
			this.addChild(bar);
			this.bar.addChild(new imgBar());
			
			// nob
			this.nob = new Sprite();
			this.nob.buttonMode = true;
			this.addChild(this.nob);
			this.nob.addChild(new imgNob());
			
			// move nob
			_val = (_val > _max)? _max : _val;
			this.nob.x = this.bar.width * (_val / _max) - nob.width/2;
			
			// drag area
			this.dragArea = new Rectangle(0, 0, bar.width-nob.width, 0);
			
			// click to bar
			this.bar.addEventListener(MouseEvent.MOUSE_DOWN, clickHandler);
			
			// D&D to nob
			this.nob.addEventListener(MouseEvent.MOUSE_DOWN, dragHandler);
			this.nob.addEventListener(MouseEvent.MOUSE_UP, dropHandler);
			
		}
		
		private function clickHandler  (evt : MouseEvent) : void{
			var _x:Number = evt.localX;
			
			_x = (_x<0)? 0 :  _x;
			_x = (_x>bar.width-nob.width)? bar.width-nob.width : _x;
			nob.x = _x;
			
			this.callEvent();
			
		}
		
		private function dragHandler (evt : MouseEvent) : void {
			this.dragging = true;
			var target : Sprite = evt.target as Sprite;
			target.startDrag(false, this.dragArea);			
			target.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove);
			
			this.callEvent();
			
		}
		
		private function dropHandler  (evt : MouseEvent) : void{
			if(this.dragging){
				var target : Sprite = evt.target as Sprite;
				target.stopDrag();
				target.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMove);

				var _x : Number = this.mouseX - nob.width/2;
				_x = (_x<0)? 0 :  _x;
				_x = (_x>bar.width-nob.width/2)? bar.width-nob.width/2 : _x;
				nob.x = _x;

				this.callEvent();
				
				dragging = false;	
			}
		}
		
		private function mouseMove (evt : MouseEvent) : void {
			if(this.dragging){
				var _x : Number = this.mouseX;
				_x = (_x<0)? 0 :  _x;
				_x = (_x>bar.width-nob.width/2)? bar.width-nob.width/2 : _x;
				this.nob.x = _x - this.nob.width/2;
				evt.updateAfterEvent();
				
				this.callEvent();
			}
		}
		
		
		private function callEvent() : void {
			var se : SliderEvent = new SliderEvent(SliderEvent.CHANGE);
			se.value = this.value;
			dispatchEvent(se);
		}
		
		public function get value() : Number {
			var max : Number = this.maximum;
			var min  : Number = this.minimum;
			var abs_min : Number = (min<0)? -min : min;
			var totalVal  : Number = max + abs_min;
			
			_value = (totalVal * (this.nob.x / (this.bar.width-this.nob.width))) - abs_min;
			_value = (_value<min)? min : _value;
			_value = (_value>max)? max : _value;
			return _value;
		}
		
		public function set value(val:Number) : void {
			this._value = val;
		}
		
	}
}

SliderTest.as

Sliderのテストコードです。Sliderのchangeイベントを取得して、値を出力しつづけるもの。log関数はいつものやつ。

package {
	import flash.display.Sprite;
	import flash.events.Event;

	public class TestSlider extends Sprite{
		
		public function TestSlider() : void {
			var slider : Slider = new Slider();
			slider.addEventListener(SliderEvent.CHANGE, changeValueHandler);
			this.addChild(slider);
		}
		
		private function changeValueHandler(evt : Event) : void {
			log(evt.target.value);
		}
	}
}

完成

こんな感じになります。値はFirebugに出てくるのでFirefox/Firebug必須。他の環境だと何が何だかよく分からない可能性が高いですけど。。。

まとめ

実際に作るとそこまでハマることなくさくさく作れました。パーツの座標調整が一番面倒ですかねぇ。イベントの委譲も割と簡単にできるし、いろいろ応用が効くネタにできそうです。

Flashの新フォーマット「XFL」に注目

ActionScript界の神であるColin MoockさんのBlogからの情報。

Flashの次のバージョンCS4で、今までのフォーマットの「.FLA」とは別に「XFL」なるフォーマットが用意されるようです。(via XFL: Flash's New Source Format / moockblog) これ、今まで謎フォーマットであったFLAに変わる、完全オープンな仕様のSWF生成フォーマットのようで、なかなか気になる感じだったので以下ざっくりまとめ。(一部意訳)

  • CS4ではXFLフォーマットでexport, importできる
  • XFLは基本的にzipフォーマットで素材(ASのコードや画像?)をアーカイブさせたもの
  • アーカイブの中のファイルやフォルダ構造は一緒に梱包したXMLで記述
  • このXMLの構造については詳細は今のところ未定。でもadobeとしては公開する予定
  • PhotoshopでXFLの中の画像を直接編集したり、プレゼンテーションのドキュメントのテキストを抽出したり、なんかが自由にできるよ!
  • XFLは最終的にはSWFを書き出すためにはCS4で読み込まなきゃいけない。XFLからswfのコンパイラの橋渡しはJSFLスクリプトで簡単にできるようになる予定
  • XFLから直接SWFを書き出すコマンドツールをadobeが出してくれたら、開発者はオレオレFlash CS4のようなオーサリングツールを作ることもできる!

というわけで、オープンソース化された製品専用のサイトも立ち上げて、オープン化がどんどん進んでるadobeですが、XFLについてもこの流れの一環なんでしょうかね。最後の下りでColin Moockさんも言ってますが、XFL2SWFがadobeから提供されるかどうかがポイントになりそうです。これ提供されたらAIR開発者はヨダレものなんだろうなぁ。あと、XFLについては、アニメーション作成のタイムライン派のユーザもなんとかできるものになるのかも気になるところ。このあたり含めてCS4についてはウォッチしていきたいと思います。

JSONの文字列リテラルはダブルクオートしか使えない

{ "name" : "katsuma"}

はOKだけども

{ "name" : 'katsuma'}

はダメ。細かい!><

JavaScriptの中だけで完結してたら特に怒られないので気づかないんだけども、ActionSciriptでJSON扱ってるときに何も考えなかったらドハマリすることがある。最近も小一時間これにハマってた。具体的に言うと、たとえばcorelibパッケージのJSONデコーダ使うときに、シングルクオート使うとパースエラーになってコケてしまう。

そもそも仕様は?

今更ながら見直してみると、Stringの定義はちゃんとダブルクオート使えってちゃんと言ってますね><

あまりに基本すぎてスルーしてると痛い目にあう、典型的な話でした。

ActionScript3でFLVをストリーミングでリピート再生

RTMPストリーミングでリピート再生の話。ActionScript2の時と書き方が変わってて混乱したので備忘録。NetStream.clientオブジェクトにハンドラを設定した独自のオブジェクトを参照させてあげないとダメだった。

次のコードをRTMPHandle.asで保存。

package {
	import flash.net.NetStream;

	public class RTMPHandle {
		
		private var stream:NetStream;
		private var session:String;
	
		public function RTMPHandle(stream:NetStream, session:String){
			this.stream = stream;
			this.session = session;
		}
		
		
		public function onPlayStatus(info:Object):void {
			if(info.code=="NetStream.Play.Complete"){
				this.stream.play(this.session);
			}	
		}
		public function onMetaData(info:Object):void {
			
		}
		
	}
}

その上でリピート再生させてやりたいNetStream#clientに対してRTMPHandleを参照させる。

var ns  : NetStream = new NetStream(nc);
var session : String = "megane";
ns.client = new RTMPHandle(ns, session);

これでリピート再生ができる。onMetaDataで空のメソッドを定義してるのは、これを定義していないとdebug用のFlashPlayerを使っていると警告(エラーだっけかな?)が出て目障りだったから。普通のFlashPlayerを使っている分には特に問題は無さそう。

Wowza Media Serverの使い方(実装編)

またまたWowza Media Serverについて。前回のつづき。今回はサーバサイドで独自のロジックを組み立てるときのお約束だったり、ASとの連携方法などについての話です。

ExternalInterfaceでActionScriptの関数呼び出し失敗への対策

[2008.11.19 追記]
関連エントリーとして「ExternalInterfaceでは対象swfをonLoad以降にロードしてはダメ」を投稿しました。

FlashPlayer8からExternalInterfaceを利用することで、かなり簡単にASからJSの関数を呼び出したり、JSからASの関数を呼び出すこともできるようになりました。で、JSからASを呼び出す場合は、あらあじめAS側でJSから呼び出す関数の名前と、実際に実行する関数の登録を行うことで可能になります。たとえばこんな感じ。

ExternalInterface.addCallback('setMessage', this._setMessage);

これだとJS側でswfのオブジェクトを参照してsetMessageを呼び出すと、AS側で_setMessageが呼び出される仕掛けになります。このときにやってみて初めて体験するハマりポイントが多いので、今日はそのポイントのまとめの話。

Wowza Media Serverの使い方(入門編)

Flash Media Serverの(ほぼ)完璧なクローンとして、Wowza Media Serverというものがあります。元Adobe(しかもFMS担当だった気が)の社員がスピンアウトして立ち上げたもので、Javaで書かれていてMacでも動いたり、614,250円払わなきゃオリジン、エッジサーバの構成ができないFMSと違って$995で全てがそろってしまうナイスなストリーミングサーバです。詳細な違いは糸柳さんが詳しい説明を書いてくださっているので、そちらが大変参考になります。

ちなみに同じくFlashのストリーミングサーバでオープンソース版でRed5もありますが、以前のエントリーのとおり、FlashPlayerのバージョンで動作が異なったり、SharedObjectの挙動が怪しいときがあったりと、まだまだ安定さは欠ける模様です。

Wowza Media Serverのインストール

このWowza Media Serverですが、インストール方法は非常に簡単です。Windowsならインストーラに従って進めるだけ。Linuxは実行権限を追加して、自己解凍形式ファイルを実行するだけです。僕はcoLinux上のFedora7に入れてみました。

sudo chmod +x WowzaMediaServerPro-1.3.3.rpm.bin
sudo ./WowzaMediaServerPro-1.3.3.rpm.bin

/usr/local/WowzaMediaServerPro-1.3.3にインストールされてあるので、binに移動してライセンスキーを入力します。ライセンスキーは試用版の場合は利用許諾書に同意してメールアドレスを登録することで、取得することができます。

cd /usr/local/WowzaMediaServerPro/bin
./startup.sh
(ここでライセンスキーを入力)

起動スクリプトの設定

その後、chkconfigでサービスに登録しておくと便利です。

sudo /sbin/chkconfig --level 345 WowzaMediaServerPro on

こうしておくと、httpdやmysqldと同じようにinit.dスクリプトに追加されます。

sudo /etc/init.d/WowzaMediaServerPro (start|stop|restart)

アプリケーションの作り方

ActionScriptでストリーミングサーバに接続するためには、Wowza Media Server側でアプリケーションの設定を行います。アプリケーションをここで「tv」とすると、ディレクトリの構成は次のようになります。

ActionScriptで使うlog関数のエラー対策

ActionScript3で使うと便利なライブラリでlog関数があります。

log(s);

で呼び出したときに、Firebugがインストールされてあると、console.log(s)が実行される仕掛けです。

ただし、この関数はFirebugがインストールされてあることが前提のものなので、たとえばIEを利用している場合は表示がおかしくなる場合があります。(真っ白になったり。)なので、console.logを呼び出す際にうまくラップしてあげた方がベターです。たとえばこんな感じ。

Airworksが素晴らしい

basculeさんがAIRのデモで作ったAirworks3.「Fireworksのインターフェースをしつつも短形を描くとそこがブラウザになる」という内容だけ聞いてて、一体どんなものだろう。。。と思ってた矢先、ついにその動作の様子が公開されました。それがこれ。

Airworks CS3

す、すごすぎます。。。

これは本当に実際に使ってみたすぎる。結構スペック要求されるかもしれませんが、画期的すぎるUIとその動作のインパクトは個人的にiPhoneクラスです、これw ブラウザにフィルタかけたまま動かす、なんて発想が素晴らしいなぁ。AIRは本当に時間とってちゃんと勉強したいのに、なかなか他の業務で時間が割けていないのが悔しすぎます。

FLVPlaybackで「.flv」以外のファイルを有効にする

FLVファイルの再生を行うためのFLVPlayerを作るにはFlash8 Proに用意されてあるFLVPlaybackコンポーネントを利用すると簡単です。スキンもあらかじめいろいろな種類のものが用意されてあるし、カスタマイズもできるので、なかなか使い勝手がよいです。

ところがこのFLVPlayback、かなりクセがあって、再生対象となるファイルを指定するときに「.flv」で終わる拡張子のファイルパスでないと受け付けてくれません。たとえば動的なURLで、実際はFLVファイルを返すようなパスであっても、内部でreturn falseとなり、再生を行うことができません。この問題は結構知られてあって、対処方法としては旧式のコンポーネントであるMediaDisplay、またはMediaPlaybackコンポーネントを利用する方法が知られています。

実際、このMediaDisplay、ないしMediaPlaybackを利用することで上記の問題は解決できるのですが、MediaPlaybackのコントローラのUIは(個人的に)やや微妙で、少し扱いづらい印象です。また、FLVPlaybackの方が、名前にFLVがついている通り、FLVファイルの再生に関してはかなり便利なメソッドやイベント検出などが多く用意されてあり、なんとかFLVPlaybackを利用したいかぎりです。

と、いうわけでFLVPlaybackの拡張子縛りは何とかならないのかな、、と思ってかなり調べている中でなんとか対応できました。以下、その手順メモです。

「F-site Adobe Flash CS3 Professional ことはじめ」に参加してきました

F-site Seminar

F-site主催のセミナー「Adobe Flash CS3 Professional ことはじめ」に参加してきました。Flashオンリーのセミナーに参加するのは初めてです。AS3はフリーでのコンパイラ環境は整っているのですが、それでもはやりGUI上で操作できるCS3環境は気になる存在。と、いうわけで以下、セミナー上でのメモです。

FLVファイルの擬似ライブストリーミング

Flash(FLV)でライブストリーミングを行う場合、FME( FlashMediaEncoder ) + FMS( Flash Media Server )の組み合わせが、一番簡単な方法です。FMSはRed5で置き換え可能なので、完全にフリーソフトウェアのみでの環境構築も可能です。

しかし、現状はFMEはソースとして外部入力のみを受け付ける仕様になっています。 WME(Windows Media Encoder)の場合、ファイルを利用したストリーミングが可能なので、すでに映像編集を終えたファイルをテレビ的にストリーミング配信を行うことが可能なのですが、FMEの場合は同様のことを行うことができません。 かと言って、FLVをWebサーバに配置していても、クライアントのFlashPlayerからの接続要求に対しては、オンデマンドストリーミングとなってしまい、ある時刻におけるユーザが見ている映像は、各ユーザ毎に異なるものとなります。

そこで、FLVファイルをライブストリーミング配信するにはどうすればいいかと言うと、 Webサーバの時刻に応じてクライアントの再生ポイントをズラし(つまりシークし)、あたかも ライブストリーミングを行っているかのように振舞わせる、ということでFLVファイルの擬似ライブストリーミングを行うことができます。

アイディアさえひらめけば、ロジックも単純で実現しやすいのですが、実際は結構ハマるポイントもあったので、備忘録として手順をまとめておきたいと思います。

ExternalInterfaceを使ったFlashの呼び出し元制御

ActionScriptでExternalInterfaceを利用するとActionScriptからJavaScriptを呼び出せることができるのですが、普通に使うとJavaScript側で関数定義を行っておく必要があります。


たとえばActionScript側で

var ua = ExternalInterface.call("getUserAgent");

のように呼び出し、JavaScript側で

function getUserAgent(){ return navigator.userAgent; }

のような定義を行っておく必要があります。


あるに決まってます。。と今日の今日まで思ってたのですが、よく考えたらcallの引数って無名関数でもOKな、はずなんですよね。実はこれって結構便利。

ActionScriptのコンフリクトエラー

ActionScriptで自前のクラス定義を行い、Flashでそのクラスを利用してコンパイルを行おうとすると
「クラス名のコンフリクト:~.as: 行 ~:このクラスの名前 '~' は、ロードされた別のクラスの名前 '~' とコンフリクトします。」
なんてエラーが発生するときが相当あります。シンタックスも、importも問題ないはず!にも関わらずこんなものが起こることがほとんとで。これ軽くググるとかなり有名な問題みたいですね。。これについてのエントリーが出るわ出るわ。

そんな僕も相当ハマりましたが、これをなんとか回避するためのバッドノウハウをメモします。

Index of all entries

Home > develop > ActionScript Archive

Search
Feeds

Return to page top