preload
4月 23

初期投資を抑えつつサーバインフラを手軽に拡大/縮小できる、いわゆるクラウドサービスが人気みたいです。Amazon EC2 みたいなやつ。聞くところによると、ソーシャルアプリなどは、アクセス規模とかユーザの増加傾向が見積りにくいようで、そういうところで利用が進んでいるらしいです。

ところで、そういったサービスは、何もサービスを受け付ける側を作るのに使うだけでなく、(負荷試験やパフォーマンスチューニングのための)Web アクセス負荷を大量に発生させるインフラとしても使えるんでないかなあと思っていたら、やっぱりそういうモノがありました。JMeter in the Cloud という AMI (Amazon Machine Image) です。

JMeter In The Cloud – A cloud based load testing environment

元々 JMeter では、複数のホストに JMeter を入れておき、JMeter Master/Slave という構成をとることで、Master から複数の Slave に対してテストシナリオ(テストプラン)の実行を指示することができるようになっています(下記リンク参照)。ですが、上のリンクで紹介されている AMI を使うと、JMeter を使った(超高負荷を発生させる)分散負荷テスト実行環境を、Amazon EC2 上に、驚くほど簡単に(慣れれば10分もかからずに)構築できます。同時10,000接続とか、それ以上のリクエスト環境をサクッと作れちゃいます。

参考:

使い方は、上に書いた AMI 紹介ページに、PDF が置いてあるので、それを読めばわかります。とても簡単。以下、その PDF に書いてあることを試したので、そのログをそのまま紹介します。

注意

以下の手順では、簡単に膨大な量のアクセス負荷を発生させることができます。悪用厳禁はもちろんですが、たとえば、単体ホストから  JMeter  や ab などを使う程度の負荷で音を上げるような状況のサービスの負荷試験には向きません。下手に大量のトラフィックを発生させることは、第三者に迷惑をかけることもありますので、利用にはご注意ください。

前提知識

というほどではないですが、適当に端折りますので、以下の経験があったほうがいいかもしれません。

  • EC2でインスタンスを起動したり、管理コンソールをいじってみたりしたことがある
  • JMeterを使ったテストシナリオの作成&実行をやってみたことがある

準備するもの

  • AWSアカウント
  • NXクライアントソフト(後述)
  • 負荷試験対象となるサービス
  • お金(EC2でインスタンスを起動してる時間に応じてかかる。1インスタンスあたり0.085USD~/h)

概要

JMeter in the Cloud は、ドイツのIT Beratung Jörg KalsbachというITコンサルティング会社の方が公開している AMI です。このイメージの正体は JMeter In The Cloud というデスクトップアプリケーションがインストールされた Ubuntu (8.10) です。このイメージを使うことで JMeter を分散環境で利用しようというわけですが、全体の流れは以下のようになります。

  1. 上記イメージを EC2 で起動します
  2. 起動したホストに、この後紹介する NX クライアントソフトで接続すると、リモートで X Window System のデスクトップ環境が利用できます
  3. デスクトップに置かれた JMeter In The Cloud というアプリを起動し、JMeter Slave となるインスタンス数と AWS のアクセスキー&秘密鍵を指定します
  4. JMeter Slave ホストが指定数分起動し、リモート利用設定がなされた状態で JMeter GUI が起動してきます
  5. 起動した JMeter GUI でテストプランを設計し、Remote Start All などとすることで、JMeter Slave に対して一斉テストを指示します

以下、スクリーンショットぺたぺたしながら紹介します。

実行の流れ

NXクライアントソフトのインストール

NXというのは、今回初めて知ったのですが、X Window System をネットワーク越しに利用するためのテクノロジで、VNC と同じようなものだと思うのですが、比較的狭いネットワーク帯域(遅い転送速度)でも利用できるのが特徴らしいです。紹介したものの、Wikipedia に書いてあることを引用しただけで、それ以上のことはよくわかりません。冒頭紹介したリンク先にあるマニュアルで、この技術の開発元である NoMachine(イタリアの会社らしい) が紹介されています。というわけで、そこのリンク先(以下)から NX クライアントソフトをダウンロードして、インストールします。

NX Download – NoMachine

Screenshot-8

NX Client Products というところから、利用環境に合わせてダウンロード&インストールします。僕は Mac OS X(10.5)版と Linux(DEBパッケージ)版を試してみましたが両方問題なく使えました。

EC2でのSecurity Group設定

次に、ブラウザで AWS の管理コンソールに接続し、Security Group を作成します。Security Group というのは、複数のインスタンスの集合単位で、これを設定しておくと、グループの内外でネットワークレベルでのアクセス制御ができたりします。作成済みの Security Groups は管理コンソールの左側のメニューから一覧できます。(今回紹介している JMeter in the Cloud は US East か EU West の Region でしか使えないみたいです。できれば US West で使いたいですが、今のところ調べきれてません。。)

Screenshot-6

JMeter in the Cloud では、”All Incoming” という名前の Security Group を決め打ちで要求しているので、上の方にある “Create Security Group” で、グループ作成します。

Screenshot-7

ここでは、Descriptionは上のように書いてますが、何でもOK。

JMeter in the Cloud イメージ起動

では、JMeter Master となるメインのインスタンスを起動します。

Screenshot-3

“Launch Instance” のメニューから、Community AMIs のタブで、AMI を検索します。US East であれば ami-0cf21065、EU West であれば ami-9b9fb4ef で検索してください。見つかったイメージの右側の “Select” ボタンから起動していきます。いろいろ起動オプション選べますので変えたい場合は変えてください。適当にぽちぽち進んでいけば OK。

Screenshot-4

Screenshot-14

Screenshot-15

Security Groups は、さっき作成した All Incoming でもいいんですが、その場合、All Incoming のネットワークポリシをちょっといじってやる必要があるみたいです。そのままだと、この後の NX クライアントからの接続がうまくいきません。マニュアルには TCP 22,80,1099 と UDP 161 をあけろ、と書いてあるので、多分、これでしょう(試してない)。僕は、上の図のとおり Default を選択しました。

この後、確認画面が出て、ボタンを押すと、インスタンスが起動します。ここから時間課金です。こういうのに、未だにドキドキしてしまう。

起動したら、My Instances 一覧画面で割り当てられた FQDN 名をチェックしておきましょう。この後の接続先設定に使用します。

Screenshot-10

起動したイメージへの接続

既に説明したとおり、起動したイメージへは NX クライアントを使用します。先ほどインストールした NX Client を起動して、まずは接続(セッション)設定を作成します。

Screenshot-9

Screenshot-11

セッション名は適当です。接続先ホストには、先ほどチェックした起動済みインスタンスの FQDN 名を指定します。

Screenshot-12

このあとの画面で Finish すれば、接続先作成は完了。この設定を使って、EC2 上で起動しているリモートのインスタンスにログインします。

Screenshot-13

起動済みインスタンスは、ログインユーザ/パスワードが作成済みです。ともに jmeter と指定すればログインできます。

Screenshot-17

こんな感じで、リモートマシンのデスクトップが手元にやってきます。EC2 でデスクトップ環境を動かしたのが初めて(いつもは CLI しか使用しない)なので、これが US East で動いてるとおもうと、なぜだか感動。

JMeter In The Cloud アプリの起動

上のデスクトップに置いてあるアプリを起動すると、JMeter Slave として起動するインスタンス数と、AWS のアクセスキー/秘密鍵を入れるダイアログが表示されます。このアプリを介して、JMeter Slave インスタンスを起動する、というわけです。

Screenshot-18

ここで起動させるインスタンスは m1.small タイプ($0.085/hour)になります。Number of Instances to start は、起動するインスタンス数。10って入れたら10個起動します。課金スピードは10倍です。ちなみに、AWS key やら AWS secret key にはコピー&ペーストしたいところですが、リモート接続してるデスクトップなので、ローカル環境からはコピーしてこれません。僕は Firefox 起動して AWS アカウント管理画面に接続して、そこからこぴぺしました。なんかでクリップボード共有もできたような気がしたけど忘れた。

Screenshot-19

起動にはだいたい2分くらいかかるよ、と出ます。が、起動した数量によっては、もっとかかります。あと、先ほど AWS の管理コンソールから作成した “All Incoming” Security Group が無いと、ここでエラーが出ます。

Screenshot-21

うまく起動すれば、こんな感じに、JMeter の GUI が表示されてきます。やった!

Screenshot-22

リモートホスト(JMeter Slave)に先ほど指定した10個のホストが登録されていることもわかります。

起動後に、AWS の管理コンソールで My Instances をみると、ばっちり起動しまくっています。これらは、上で起動した JMeter を動かしている限り動きつづけます。

Screenshot-20

JMeter でテストプラン作成&実行

あとは、JMeter そのものの使い方になるので、マニュアルとか他の解説サイトを参考にしてください。以下は、僕の管理している某サーバに HTTP GET を 10 Threads, 10 Loop で発行するスレッドグループを作成して、これを Remote Start All したときの Graph Result です。10 インスタンス起動しているので、10 x 10 x 10 で 1,000 リクエストが飛んでいます。

Screenshot-23

JMeter は、テストシナリオをかなり細かく設定していけますし、乱数発生やif分岐、foreachループなどちょっとしたスクリプティングもできてしまうので便利ですね。リポート形式もいろいろあるのもありがたいです。

細かいプランはローカルマシンの GUI で作って xml 出力しておいて scp で運んでも良いと思います。ブラウザが使えるので、その辺のファイルの共有は、Dropbox など使うのもアリかも。

テストが終わったら、忘れずにインスタンスをシャットダウンしておきましょう。リモート起動した JMeter Slave は、ここで起動した JMeter GUI を終了することで、終了処理がコールされるようになっています。

まとめ

JMeter in the Cloud 便利だな!という話でした。これがパブリックイメージで無償で利用できるというのは驚きです(寄付歓迎とのこと)。いつ有償になってしまうか、突然請求書が届いたりしないか不安ですが。。

しかし、こんなかんじで、数時間負荷をかけるための利用であれば、10,000円でお釣りが来てしまうくらいなんですね。便利な時代になったものです。

(2010/05/04追記: Amazonから先月利用分の請求書が来ましたが、このために利用した分の請求額は $2.28 でした。2, 3 の負荷テストを実験してみたり、このエントリ書くために 1, 2 時間起動しっぱなしでキャプチャとったりしてましたが、概ね、こんなものですね。安い!)

Tagged with:
7月 27

先日、SwfmillRuby に hokaccha さんのブランチをマージしました。hokaccha さんのブランチでは、32bit png の image2xml に対応いただいておりましたが、これに xml2image の処理を追加実装しています。これにより、32bit png (DefineBitsLossless2 の image format=5) に暫定対応しました。

ただし、今回追加実装した xml2image は、透過色を含む PNG の扱いについて完全ではないので、あくまで暫定対応という位置づけです。32bit png を SWF 内部のビットマップに変換する際に RMagick(ImageMagick) を経由して得られる各ピクセルの情報と、Flash IDE がパブリッシュするビットマップの各ピクセルの情報とで、微妙に誤差が出てしまっており、現時点で、Flash IDE が出力するビットマップデータと完全に一致させる事ができていません。

具体的には、DefineBitsLossless2 の ARGB データから RMagick::Image の png イメージを作成する部分が怪しい感じです。確証はないのですが、どうも、SWF File Format Specification にある:

The RGB data must already be multiplied by the alpha channel value.

の “multiplied” の解釈が単なる乗算ではないみたいような気がしています(除算で逆変換しても適当な数値にならないのです)。今のところ、幾つか透過色を含む PNG で動作検証してみた限りでは、ここを逆変換する際に、RGB それぞれの値と opacity との OR を取る事で、元のイメージの再現性がある程度確認できたので、ひとまずこの方法で実装してみました。何か勘違いしている気がしないでもないのですが、、お気づきの方、コメントいただけると幸いです。

ちなみに、上記の更新以外でも、partialize や templatize の機能を追加し、partizlize や templatize において、SWF を再生成(regenerate)する際の XML 処理コストを少しでも削減するための事前処理をおこなえるように改修を続けています。これは、ちょっと複雑な SWF の動的生成/書き換え/合成をしようとすると、再生成に XML 中の ID 体系を付け替えたり、要素を入れ替えたりといった処理のコストがとても大きくなる事がわかってきたからです。詳しくドキュメントを書く時間が取れていないのですが、サンプルやソースコードコメントから意図を汲んでいただけると幸いです。(少なくとも FlashLite 1.1 の携帯サイト向けには、わりと面白い事ができるくらいのものにはなってきていると思います。)

まだ公開して間もない SwfmillRuby ですが、いつの間にか、いくつかの方面からご利用のご連絡をいただいており、とても励みになっております。ライセンス上、ご利用のご連絡は必須ではありませんが、よろしければご利用コメントやご意見などお寄せいただければとうれしいです!

Tagged with:
5月 28


2009/06/04追記: こちらのサンプルコードは、その後のバージョンアップにより動作しないものが含まれています。大きな変更点としては「テンプレート」と言葉の用法を変更し、これまで「テンプレート」と呼んでいたものを「パーツ」と呼ぶようにしています。詳細は配布ファイルに含まれるテストコードもしくは別エントリ(そのうち書くかも)をご参照ください。

先日こちらで紹介した SwfmillRuby をバージョンアップして、いろいろ便利機能を追加してみました。機能追加以前に、SWF 内のタグの対応数が絶対的に少ないのですが、基本的に SwfmillRuby が対象にしているのは FlashLite 1.1 でパブリッシュされた SWF なので、この用途に限れば、そこそこ使えるものになってきたと思います。ライセンスに変更はありません(GPL2)。無保証です。

今回追加された機能は、大きく次の3点です。

  • ムービークリップの検出と入れ替えに対応
  • 入れ替えのための事前処理として、ムービークリップをテンプレート化しておけるようにした
  • XML Parser を rexml から libxml2 に変更

上記のほか、SwfmillUtil::Swf 初期化の方法が変更されていたり、SwfmillUtil::DefineSprite というクラスが追加されていたりといった、こまごまとした修正が入っています。が、できるだけ外側からは、xml 使って云々とか、SWF のタグがほげほげということは意識しなくても良いように作っているつもりです(といっても、まだまだうまい書き方できそうなところは多いですけど)。

では、使い方は、アーカイブ中の sample を見ていただくとして、以下、それぞれ、日本語で解説していきます。

ムービークリップの検出と入れ替えに対応

Swf#movieclips により、SWF 内のムービークリップのリストが取得できるようになりました。SWF 内部で採番されているID => SwfmillUtil::DefineSprite のハッシュの配列が返ります。

これを使って、以下のような形で片方の SWF のムービークリップを、もう片方の SWF のムービークリップと差し替える、というようなことができます。

require '../lib/swfmill_util'
require 'pp'

################################################################################
# test to replace movieclip

# initialize
swf = SwfmillUtil::Swf.parseSwf(File.open("data/sample_original.swf").read)
swf2 = SwfmillUtil::Swf.parseSwf(File.open("data/sample_original2.swf").read)

# check included movieclips (object_id => SwfmillUtil::Swf::DefineSprite)
pp swf.movieclips.keys #=> ["8", "5"]
pp swf2.movieclips.keys #=> ["6", "3"]

# check included movieclip_ids by instance_name
pp swf.movieclip_ids_named("animation") #=> ["5"]
pp swf2.movieclip_ids_named("animation") #=> ["6"]

# replace movieclip
swf.movieclips["5"] = swf2.movieclips["6"]

# write swf replaced movieclip
swf.write("data/replaced_mc.swf")

上の例では、まず、Swf#movieclips により、ふたつの SWF に含まれるムービークリップの ID 体系を確認しています。ムービークリップの ID を確認する際は、実用上は、そのうしろの行にあるような、Swf#movieclip_ids_named が便利かもしれません。これにより、ステージ上に置かれる際に付与されたインスタンス名を指定し、対象のムービークリップの ID が確認できます。

実際の入れ替えは、上のように、ハッシュを操作するような形式でおこなえます。ムービークリップは、内部で各種形状(シェイプ)や画像(ビットマップ)、テキスト情報などを参照していますが、入れ替え自体は、上のようなハッシュへの代入操作一発で、参照関係にあるリソースを丸ごと入れ替える事ができます。また、このとき、単純に上書きしてしまうと、内部で使用している ID が重複してしまうなど、ID 体系が狂ってしまうことになるので、Swf#write (のなかで呼ばれる Swf#regenerate) したタイミングで、よしなに ID の体系を調整(adjust)する処理が走るようになっています。

入れ替えのための事前処理として、ムービークリップをテンプレートパーツ化しておけるようにした

これは、FlashLite コンテンツの動的生成サイトを作ろうとした際、実用上必要になる事が多いので、ちょっと強引に作ってみた機能です。実際に、前節のようにムービークリップの入れ替えをおこなうと、内部的には SWF を XML に戻し、XML を書き換えて、XML を SWF に戻す、ということをやることになるため、Swfmill プロセスの起動コストがかさんでしまいます。また、ムービークリップの入れ替えでは、関連するリソースも一気に処理対象になるため、前節最後で書いたような、入れ替え後におこなう ID 体系の調整コストも無視できなくなってきてしまいます。

そこで、あらかじめ、入れ替え対象となる(元の) SWF と、そこに差し込むムービークリップをあらかじめ調査しておき、差し込みをおこなうムービークリップをテンプレートパーツ化しておくことを考えてみました。

以下は、対象の SWF を調査し、片方の SWF に含まれるムービークリップをテンプレートパーツとして保存しておく際のコードサンプルです。

require '../lib/swfmill_util'
require 'pp'

################################################################################
# test to templatize movieclip                                                                                                                              

# initialize
swf = SwfmillUtil::Swf.parseSwf(File.open("data/sample_original.swf").read)
swf2 = SwfmillUtil::Swf.parseSwf(File.open("data/sample_original2.swf").read)

# check included movieclips (object_id => SwfmillUtil::Swf::DefineSprite)
pp swf.movieclips.keys #=> ["8", "5"]
pp swf2.movieclips.keys #=> ["6", "3"]

# check included movieclip_ids by instance_name
pp swf.movieclip_ids_named("animation") #=> ["5"]
pp swf2.movieclip_ids_named("animation") #=> ["6"]

# templatize movieclip specifying the mapping of object_ids
#  and available, unused object_id (if you want to adjust object_ids)
File.open("data/animation_template.xml", "w") do |f|
  f.puts swf2.movieclips["6"].templatize(true, 6, 5, 1000)
end

上の通り、DefineSprite#templatize は 4 つの引数をとります。これらは手前から:

  • あらかじめ ID の調整をおこなうか(true/false)
  • 対象のムービークリップに付与されている ID
  • 対象のムービークリップを、SWF に入れ込む際の ID
  • テンプレート化の際におこなう ID の再編成時に使用できる、未使用の ID の最小値

を表しています。真ん中のふたつは、上記の前半で得られるような、SWF の調査結果をもとに指定します。最後の引数は、明らかに重複の無い大きめの値をバッファを持って指定しておくと良いと思います。なお、引数を何も指定しなければ、テンプレート化の際に ID の調整はおこないませんので、regenerate の際に調整する必要が出てきます。

なお、ここでは、テンプレート化されたムービークリップは XML 化してファイルで永続化していますが、DB に突っ込んだり、容量が許すなら memcache などのキャッシュに載せておいたりという手を使っても良いと思います。

こんなかんじで作成したテンプレートを、以下のようなコードで差し込みます。ここでは、あらかじめ Swfmill#swf2xml した xml から初期化することで、元の SWF を読み込む際に Swfmill 起動が必要ないようにしています。

require '../lib/swfmill_util'
require 'pp'

#################################################################################
# test to regenerate swf using template movieclip

# initialize original swf by preserved xml generated by Swfmill::swf2xml.
# avoid analysing swf's structure using "template_mode".
swf = SwfmillUtil::Swf.parseXml(File.open("data/sample_template.xml").read, true)

# initialize template movieclip by preserved xml generated by Swf#templatize
# avoid analysing swf's structure using "template_mode".
animation = SwfmillUtil::DefineSprite.parseXml(File.open("data/animation_template.xml").read, true)

# check a target object_id
pp swf.movieclip_ids_named("animation") #=> ["5"]

# change movieclip
swf.movieclips["5"] = animation

# write swf changed movieclip.
# avoid adjusting object_id using "adjustment=false"
swf.write("data/regenerated.swf", false)

初期化時の最後の引数は、テンプレートモード (template_mode) を表しています。テンプレートモードで Swf を初期化すると、初期化時に SWF の構成チェック処理がおこなわれません(つまり、Swf#images や Swf#movieclips で、現在の構成をダンプすることができません)。これにより、初期化の際の処理ステップを、ある程度スキップする事ができます。テンプレートモードは、templatize により、テンプレートがあらかじめ作ってある&元の SWF が調査済みで、入れ替えをおこなう相手もわかっているというときに限り、使用される事を想定しています。

また、Swf#write の最後の引数で指定している論理値は、入れ替え時の ID 調整(adjustment)要否を表しています。templatize の際に adjustment=true とし、入れ替え後の ID 調整を先におこなってあれば、ここで adjustment=false とすることで、さらに Swf 生成前のコストを削減する事ができます。

このあたり、冒頭にも書きましたが、ちょっと強引な仕上がりになってますので、ぱっと見た感じ、何が起こるのかわかりにくいのが難点です。もっと使いやすいインタフェースを考えてますが、今のところはこんな感じでご容赦ください。

XML Parser を rexml から libxml2 に変更

オモテからはほとんど意識する必要のない変更点なのですが、これにより今回追加された機能の処理部分のパフォーマンスが劇的(概ね1桁以上、場合によっては2桁)に上がりました。libxml-ruby は Ruby の標準添付ではないので、別途インストールが必要になってしまうのが懸念点だったのですが、実用上こちらの方が望ましいと思いましたので、思い切って全体を書き換えることにしたのでした。

この乗り換えのきっかけは、このライブラリは、内部では XML 処理がごりごりおこなわれているのですが、rexml を使っていたときは、とくにムービークリップ周りの解析や、ムービークリップ入れ替えの際におこなわれる XPath 検索にかなりのコストがかかっていたことがわかった、ということでした。たとえば、50KB のムービークリップが 30 件くらい入った SWF を解析するのに、デスクトップ環境で5-6分かかり、テンプレートを使用したムービークリップの入れ替えに3-4秒かかるといった具合でした。(そもそもの書き方がアレだった部分は重々ありますが。)

解析処理をおこなう際の変数スコープを調整したり、メソッド化してあったところをブロック付きの Hash に書き換えたりすることでソコソコ改善はしたのですが、結局は rexml の XPath 検索が一番のボトルネックになっていたようで、libxml2 利用に置き換えたのが一番効果的だったようです。

libxml2 の利用により、これまで2-3分かかっていた解析処理は3-4秒、入れ替え処理も0.2-0.3秒程度まで短縮できました。同時大量アクセスのあるサイトには厳しいかもしれませんが、他の面でもいろいろ工夫をいれる余地のある環境/状況でしたら、そこそこ実用に耐え得るのかな、と思います。

以上、今回の大きな変更点を紹介しました。まだまだ不具合等内在しているかもですが、ご利用いただけましたら感想など聞かせていただけるとうれしいです。

Tagged with:
5月 08

前回までの「ケータイサイトでFlashLiteコンテンツを動的生成する」エントリで紹介してきた swfmill を使った FlashLite コンテンツの動的処理に関連して、SWF に含まれる画像やテキストを操作するための簡単なクラス集を作ってみました。

swfmill_ruby – github

swfmill と同じ GPL2(the GNU GENERAL PUBLIC LICENSE Version 2) にてライセンスいたします。

swfmill_ruby は、Swfmill を ruby から起動するための簡単なクラス(SwfmillUtil::Swfmill)と、これを使って Swf を操作するためのクラス(SwfmillUtil::Swf)から構成されます。使用には、ruby の標準的な開発環境に加えて、以下のものを用意する必要があります。

その他、使用手順など、詳細は公開ファイル中の README を参照してください。

Swf の操作機能は、現時点では、もっともよく利用する:

  • 画像の入れ替え
  • テキストの入れ替え

に絞って実装しています。これを使用することで、以下のサンプルコードのように、Swf#images で Swf 中の画像データを objectID => Magick::Image のハッシュ、Swf#texts でテキストデータを objectID => String のハッシュにてアクセスする事ができます。

require '../lib/swfmill_util'
require 'pp'

# initialize
swf = SwfmillUtil::Swf.new(File.open("sample.swf").read)

# check included images (object_id => Magick::Image)
pp swf.images #=> {"6"=> JPEG 176x208 176x208+0+0 DirectClass 8-bit 10kb, "3"=>  30x30 DirectClass 16-bit}
pp swf.texts #=> {"2"=>"343201202343201204343201206343201210343201212ABCr"}

# write included images
#swf.images.each do |i,image|
#  image.write("#{i}.#{image.format ? "jpg" : "gif"}")
#end

# replace included images
swf.images['3'] = Magick::Image.from_blob(File.open("flymelongirl.gif").read).first
swf.images['6'] = Magick::Image.from_blob(File.open("bg.jpg").read).first
swf.texts['2'] = "かきくけこXYZ"

# write swf replaced images
swf.write("foo.swf")

後半で書いている通り、画像やテキストの書き換えは、ハッシュの値を置き換えてやる事で実現できます。Swf#write によりファイル出力できますし、Swf#regenerate で再生成後の swf をそのまま得る事もできます。

内部的には、Magick::Image <=> DefineBitsJPEG2/DefineBitsLossless2 の変換をおこなっています。とくに DefineBitsLossless2 の変換処理は、ちょっと面倒ですし、あまり ruby での実装を見かけないので、何らか使いどころがあればお使いください。ImageMagick / RMagick を併用するので多少重たいかもですけど。

なお、FlashLite 1.1 を対象に、よく使うあたりを中心に実装していますので、DefineBitsJPEG3 や DefineBitsLossless2 での format=5, DefineBitsLossless, DefineText などはひとまず除外しています。必要に応じて、適当に修正してみてくださいませ。

今更ながら、大胆な名付けをしてしまった気がするので、いろいろいじってみてもらえると嬉しいです!

Tagged with:
5月 08

前回までのエントリ(第1回, 第2回)では、FlashLite コンテンツのサーバでの動的生成/合成処理について、開発ステップの全体的な流れや、swfmill を使用する際の SWF 構造の見方などについて紹介してきました。

今回は、前回のサンプルを使って、実際に swfmill を使用しながら SWF の動的生成をおこなう手順を紹介していきます。

前回の後半で、swfmill swf2xml で得られた XML をテンプレート化しておき、これを操作する方法として、次の2種類を紹介しました。

  • XML 中の置き換えをおこなう部分を場所を一意に特定できる文字列にしておき、プログラムからは文字列置換により書き換える
  • 置き換え対象部分を独自に定義した要素などに編集しておき、適当な XML 操作のための API (僕の場合は、PHP であれば SimpleXML、Ruby であれば REXML や Libxml-Ruby などを使ったりしてます) を介して書き換える

ここでは、手軽な前者の方法により、前回紹介したサンプルの背景画像の入れ替えをやってみます。背景画像は、XML 中:

182       <DefineBitsJPEG2 objectID="4">
183         <data>
184           <data>/9n/2P/Y/+AAEEp..

の data で定義されていますので、これを下のような形に書き換えてみます。

182       <DefineBitsJPEG2 objectID="4">
183         <data>
184           <data>####BACKGROUND_IMAGE####</data>

この XML を sample.xml として保存します。あとは、プログラムから、この XML の ####BACKGROUND_IMAGE#### の部分を文字列置換により JPEG 画像に差し替え => swfmill xml2swf とやれば、背景画像の差し替え完了となります。

せっかくの XML データですから、各種 XML API を介して操作するのが良いのかもしれませんが、システム内外からの不特定な入力の余地がないのであれば、こういった単純な文字列置換の方が、よりシンプルに実装でき、かつ高速な動作が期待できると思います。

今回は、ruby から swfmill を起動、XML の文字列置換をするためのクラスとして、下のような簡単なクラスを書いてみました。

module SwfmillUtil

  SWFMILL = "/Users/tmtysk/bin/swfmill"

  class Swfmill

    def self.xml2swf(xml, option = "-e cp932")
      IO.popen("#{SWFMILL} #{option} xml2swf stdin", "r+") do |io|
        io.write xml
        io.close_write
        io.read
      end
    end 

    def self.swf2xml(swf, option = "-e cp932")
      IO.popen("#{SWFMILL} #{option} swf2xml stdin", "r+") do |io|
        io.write swf
        io.close_write
        io.read
      end
    end

  end 

end

これを使って、以下のように差し替え処理を書いてみます。背景画像は bg.jpg とします。

require '../lib/swfmill_util'

xml = File.open('sample.xml').read
xml.gsub!(Regexp.new('####BACKGROUND_IMAGE####'),
            Base64.encode64([0xff, 0xd9, 0xff, 0xd8].pack("C*") +
            File.open('bg.jpg').read).gsub("\n",""))                                                                                                        

File.open('foo.swf', 'w') do |f|
  f.write SwfmillUtil::Swfmill.xml2swf(xml)
end

上記では、ファイルからオープンした jpeg データの先頭に [0xff, 0xd9, 0xff, 0xd8].pack(“C*”) の4Byteの文字列を付加していますが、これは SWF File Format Specification にも記載されている接頭子(マーカー)になります。このマーカーを忘れたり、また、Base64エンコーディングした後の改行文字を取り除くのを忘れたりすると、SWF 生成に失敗したり、SWF 生成は一見うまく行くのに再生すると画像部分が真っ赤になっていたりします。この「画像が真っ赤になる現象」は、プレイヤーが画像をレンダリングできない <= 画像データがどこかマズいという事のようで、画像の差し替えをやるとよくハマる箇所ですので、もし発生したら、上の画像エンコード周りを確認してみると良いと思います。

上のコードを実行すると、差し替え後の swf が foo.swf として保存されます。生成した swf をそのままブラウザに返すのであれば、適当な Content-Type を設定した上で SwfmillUtil::Swfmill.xml2swf(xml) の値をそのまま送信してやれば OK です。ただし、swfmill の起動コストもありますので、(程度にもよりますが)アクセスが集中するようなサイトで、同期的に SWF を生成してブラウザへ返す、というような事をやるのは、あまりオススメできません。あらかじめ SWF を作り置きしておくとか、非同期処理やバッチ処理で回避できるよう画面遷移を調整するとか、SWF 再生中のボタンアクションで非同期に loadMovie するように SWF の構成を見直すとか考える必要が出てくる事もあります。swfmill だと限界が見えてくるかもしれませんが、この他、負荷軽減のアイデアや実績があれば、コメントなどで教えていただければ幸いです。

さて、今回は、文字列置換により背景画像のみ差し替える、というやり方を紹介しましたが、同様の方法で、前回紹介したような「ムービークリップシンボルを丸ごと差し替える」というのも可能です。具体的には DefineBitsLossless2, DefineShape, DefineSprite 辺りを丸ごと文字列置換や XML 操作で置き換えてやれば OK です。この際、置き換え前後で objectID の参照関係が変わってしまわないように注意する必要があります。この辺の話も書こうと思ってたのですが、似たような話になってきたので、ここは割愛することにします。

今回使用したコードや、もうちょっと便利に使えるようにしたクラス群を github かどこかに置こうと思っています。ご興味の方、適当に遊んでみていただければと思います。

最後に、この手の事を swfmill 経由でやっていて、よく聞かれる事をまとめておきます。

Q. jpeg の部分に gif, png など他の形式の画像を置く事ってできる?

A. もっとも簡単な方法は、swfmill 外部で GD やら ImageMagick やら使って画像形式を相互変換してしまうことです。が、それだと SWF の容量制限上アレだったり、圧縮がかかって見た目上ナニだったりするので嫌、ということになると思います。そういうときは、swf2xml した後で、DefineBitsJPEG2 と DefineBitsLossless2 を置き換えてやれば OK です。jpeg 画像は DefineBitsJPEG2 で定義されていますし、それ以外の画像(gifやらpngやらbmpやら)は、すべて SWF の独自ビットマップ形式として DefineBitsLossless2 で定義されています。

DefineBitsJPEG2 では、今回の上で紹介した通り、比較的簡単な方法で JPEG データを流し込む事ができますが、DefineBitsLossless2 のデータについては SWF の独自形式ですので、既存の画像形式からの変換処理を書いてやる必要があります。この辺りの処理は、後ほど公開するクラス群に入っていたりするので、ご興味のある方はご覧ください。あまりよく確認してないですが、PHP や Perl でも実装例があったようななかったような気がしますので、そちらをお求めの方は適当に検索してみると良いと思います。

Q. 文字(テキスト)の入れ替えをやりたい?フォントを変える事もできる?

A. FlashLite1.1 の場合、テキストデータは DefineEditText(ダイナミックテキスト)要素 の initialText 属性で定義されているのがほとんどです(DefineText でパブリッシュされたのを見た事無いだけで仕様的には存在しうると思うのですが)。なので、この initialText を書き換えて xml2swf してやればテキストの入れ替えが可能です。テキストに日本語を含む場合もありますので、KLab の方が公開されている swfmill への文字エンコーディング指定パッチを適用しておくと便利です。(紹介を忘れてましたが、このパッチ、FlashLite1.1 を swfmill で扱う場合は、ほぼ必須と思います。)

フォントを変えるというのは、不可能ではありませんが、ちょっと手の込んだ事をやる必要があります。というのも、指定されうるテキストのフォント情報(グリフ)を SWF に埋め込むと、たいていの場合、それだけで FlashLite の容量制限をオーバーしてしまうからです。やるのであれば、「テキストではなく画像にしてしまう」、もしくは、「表示するテキスト情報から使用されている文字を抽出し、使用する文字の分だけグリフを埋め込む」という事をやることになるでしょう。グリフ情報はあらかじめ DB などで保持しておけば OK です。なお、フォント種類や文字にもよりますが、幾つか試してみた感じでは、日本語の漢字一文字を埋め込むと、SWF サイズが大体 300〜500Byte くらい増加するようです。

ちなみに、グリフ情報は DefineFont タグなどで定義されています。適当に1,2文字フォント埋め込みをしたSWFをパブリッシュして、構造を解析してみると良いかもしれません。

Q. ActionScript 部分を差し替えたい

A. これは、わりと僕がよくやる方法です。ActionScript のロジックは DoAction タグで定義されていますので、この子要素を SWF File Format Specification の action model 仕様を見ながら書き換えてやれば OK です。具体的には、SWF の再生フローを ActionScript 内の特定の変数値によって分岐するように書いておき、この変数値をプログラムから書き換える、といった感じです。Flashゲームや、ちょっとしたツールなど、アプリケーションライクな SWF を動的生成する際、動的な部分をある程度 SWF 側に持たせてしまうことで、生成プログラムの開発コスト/個別対応コストを下げる事も期待できます。

単に変数の置き換えをするのであれば、変数値を固定長(長さが変わってしまうと、内部で管理されているタグの長さの不一致が発生し、swf 構造が不正なものとなってしまいます)にしておけば、swf をそのまま(バイナリセーフな)文字列置換処理にかける事で、swfmill すら使わずに動的に swf を書き換える事もできます。swf の動的生成(と呼んでいいのかアレですが)としてはかなり高速な方法ですので、ゲームなど多量なアクセスが発生しうる SWF のセッション管理用にユーザ/セッション固有の ID を埋め込む、というのにも有効です。

Q. swf2xml したものを xml2swf しただけなのに、再生できなくなった

A. これはかなり稀なケースなんですが、swfmill で if / else の ActionScript コードを含む SWF を swf2xml したときの ActionScript のバイトコード変換処理に不都合があるらしく、ブロックジャンプのオフセット値が間違って出力されることがあるようです。swfmill のどの辺りかを追えてないので、根本的な解決ができてないのが申し訳ないのですが、こいつも SWF File Format Specification の action model 周りの仕様を睨みながら、出力された XML 中のオフセット値を適当に調整してやる事で、問題が解決できる場合があります。

以上、3回にわたって書いてきましたが、この手の話で、現時点で文章にできるのはこんなところです。機会があれば、今後は、こういう仕組みを使って個人的に作ったものを紹介していければ良いなあと思ってます。

何か参考になれば幸いです。

Tagged with: