AITC第2回勉強会「jQuery MobileとSencha TouchでWebアプリを作ってみよう!」のご案内

そういえば、告知が遅くなりましたが、来週末の土曜日 7月13日(土)に、下記ハンズオンで講師を務めさせて頂きます。

AITC第2回勉強会「jQuery MobileとSencha TouchでWebアプリを作ってみよう!」

概要
 実際にコードを書きながら、スマホWebアプリを作るハンズオン形式の勉強会です。2部構成で、前半はjQuery Mobile、後半はSencha Touchを使用します。同じテーマのアプリを異なるフレームワークを使うことで相互理解を深めます。

お申し込みは Facebookイベントページより。

AITC会員企業外の方は有料になります。
事前登録をすると、いまなら早割が利用できてお得です!
今週末までなので是非お早めにどうぞ〜

内容は、jQuery Mobile と Sencha Touch で同じアプリを作って、それぞれの作りの違いを実感しよう!という感じです。お題は、気象庁防災XML を使った、防災速報なようなものを予定しています。

ただ、スケジュールを見てもらうと分かるのですが、各2時間くらいしか時間が取れません。。。2時間でこの巨大なフレームワークを一通りやるのは相当にハードです。なので、細かい説明よりも、とにかく手を動かして実際に動くものを作ってみる、という感じなるかと思います。写経になるかもしれませんが^^;

細かい質問は懇親会、または @dsuket へでも。
作ったサンプルアプリはまた後ほど公開したいと思います。

是非 参加お待ちしております〜。

HTML5機能を色々使ったスマホ向けWebアプリを作ってみた話:後編

前回のエントリでもお知らせしたように、ドコモゼミ Webアプリラボのコンテストにアプリを出しました!面白かったら下記ページの いいねをお願いします!

かきまる

スマートフォンを使いながらも、アナログな感触をもっと楽しんで欲しい。そういう想いで考えられた、デジタルとアナログを繋ぐお絵かきWebアプリです。

.

で、今回のエントリでは 前回のHTML5機能を色々使ったスマホ向けWebアプリを作ってみた話:前編 で書き切れなかった残りを解説します。

今回は次のトピックス

  • かわいい文字で表示したい。
  • 音があると楽しい。
  • スマホとタブレット両方対応。AndroidでもiPhoneでも。

Webフォントでオリジナルフォントを使う

Androidがいけてないの一つが、フォントにあると思っています。標準フォントや入っているフォントやが機種によりバラバラ。しかも標準フォントがダサイのが多い。あまりに違うためデザイン的なページの場合、テキストで統一的に表現するのは難しく、画像にしないといけなくなります。

画像にすると色々問題があります。まず変更がめんどくさい。文言ちょっと変更する度に画像を書き出さないといけない。さらに、昨今の高精細ディスプレイ用に2種類の解像度の画像が必要。そしてモバイルの場合特に重要なのが、ページサイズが増えること。画像が多くなると必然的にページサイズが増え、3G回線などでのモバイルではロード時間が遅くなってしまいます。

じゃあ、どうすればいいのか。そういう時に、Webフォント が使えます。Webフォントとは CSS3 から導入された仕様で、これまではOSにインストール済みのフォントしか使えなかったのが、必要に応じてWeb上からフォントデータをダウンロードして表示できます。これを使用すると、機種毎にバラバラなAndroidでも、iOSでも、PCブラウザでも同じフォントで表現できるようになります。

ただ、日本語フォントのように膨大な数があるとフォントデータが増え、画像データよりむしろ重くなることがあるので注意が必要です。必要な文字だけのフォントデータのする、などの工夫が必要です。今回のかきまるでは、ひらがなのみなのでサイズは woff 形式で11kに収まりました。この程度なら、画像を複数枚用意するよりも少ないサイズでいけます。また、フォントデータをキャッシュさせれば、別の文字が増えたとしても新たなダウンロード時間は増えません。

前回のひらがなの書き順を表現するため、文字データをSVGで作っていたので、それを元にWebフォントにしました。Webフォントに使える形式は幾つあります。最近だと WOFF が標準ですが、古い形式のために ttf や svg形式のものも用意し、CSS記述でフォールバックするようにしておくとベターです。

SVGからWebフォントの作り方はこちらを参考にしました → アウトラインのSVGからフォントを生成 #かな書いてみるFontForge の出力形式に woff、svgなどを追加すると、自動でその形式で出力してくれます。

ちょっとはまったのが、フォントデータにするときはアウトラインデータの方が良い、ということです。まぁ、当然なのですが。。。アウトラインから変換することを前提としているので、stroke の lincap や linejoin などは無視されます。今回の文字データは、書き順アニメーションのために1本のパス(stroke)で書かれていたのですが、そのまま変換するとうまくいきませんでした。そこで、フォント用に別途アウトラインデータも作成しました。

音を再生する

photo by vancouverfilmschool (CC BY 2.0)

Webで音を再生するには二つの方法があります。簡単なのが HTML Audio要素を使うこと。スクリプトからも操作でき、簡単な音声の制御ができます。もう一つが Web Audio API を使用すること。こちらはもっと複雑なことが可能で、合成やフィルタリングなどを使用してWeb上でシンセサイザーのようなことも実現できます。

ただし、Web Audio API はモバイル端末ではまだまだサポートが厳しい現状です。iOS 6から一部制限付きで使えるようになりましたが、Androidでは全然だめ。ということで、スマホ向けWebアプリは 今のところ HTML Audio要素一択という状況です。

HTML Audio要素ならスマホでも盤石か、と言うとそういうわけでもありません。スマホの限られたリソース(CPUや帯域)では、音声データのダウンロードやデコードで少し時間がかかる場合があります。そこで予めデータをプリロードしておきたいところですが、これも色々と制限されています。詳しくは 過去エントリ iOS/Android で HTML5 の audio/video を任意のタイミングで再生する方法 でも書いたのですが、audio を play する前に、load しておくことで対応できます。

「かきまる」では、当初 最初のページにタッチしたタイミングで全音声データをロードしていたのですが、それだと重すぎて最初のページめくりが固まってしまいました。そこで、各ひらがの書き順ページへ遷移するタッチイベントをトリガーに、次の動物の名前の音声データをロードするようにしました。書き順を表示している裏でダウンロードされるため、次の動物にいったときには、すぐに再生されます。

この方法の欠点として、書き順を表示しきる前に次々にページをめくると音声再生が追いつかない、という問題がありますが、これはもう致し方なしとして諦めました。また、Android Chrome の ver 25 くらいまではこの方法でうまくいっていたのですが、最新版の 27にすると事前 load が効かず、play時にのみロードされるようになってしまいました。

他にも、Galaxy Nexus(Android 4.2.2)の標準ブラウザと Chrome を組み合わせると、ブラウザ終了時やスリープ時に最後に再生した音声が勝手に再生される、というとんでもないバグがあったりします。しかも通知でも再生されるため、メール着信などある度に ”おおありくい〜” とか勝手着信音となってしまったり(ーー;; 他の端末では起きなかったため、対処は見送り。。

スマホ/タブレット/Retina対応。Androidでも。

一通り動くようになってから、最後に スマホ(縦横)と、タブレット(縦横)でもちゃんと見えるよう、CSS メディアクエリを使って調整しました。Android でもきちんと表示されるよう対応させるのに苦労しました。

SVGを画像として使う。

画像をスマホ/タブレット用に最適化すると、それぞれのRetina用も考えると、スマホ or タブ x 2解像度 = 4パターン必要になります。さらにそれの縦横まで対応すると、1つの画像に8パターンも必要になってしまいます。(実際には縦横は同じ画像を縮小で表示することが多いでしょうが)

今回は画像に SVGを使ったため、1パターンさえあればよいので非常に楽でした。Android 2.x は SVGに対応していないのですが、ここはもうばっさり切りました。Android 4以上なら問題なし!と言いたいところですが、まだ落とし穴があります。

SVGをWebページに埋め込みで使う場合、幾つか方法があります。

  • OBJECTタグで使う
  • IMGタグで使う
  • インラインSVGにする

古いブラウザなどの場合、IMGに使えなかったりするので、OBJECTタグを使用する必要がありますが、スマホの標準ブラウザ(Chrome)ならIMGで問題ありません。OBJECTタグにする問題点は、外部ドキュメントになるため、UIイベントがそちらに取られてしまうことがあります。また、インラインSVGにすると元ページにSVG要素を埋め込まないといけないため、画像の差し替えが煩雑です。なので、単に画像として使う場合は IMG要素として使うのがオススメです。(この節のSVGと書いてある画像も実はIMGタグでsvgを指定しています)

IMGタグでSVGを使う場合、画像サイズの指定はIMGタグで行います。width/heightの属性か、CSSの属性で指定できます。通常、widthとheightを指定すると、元画像のアスペクト比を保持したままスケールします。しかし、一部のAndroid標準ブラウザでは、アスペクト比が保持されず引きのばされてしまいます。iOS と Android標準ブラウザの表示違いは下の図のようになります。

Android標準ブラウザ

そこで svg ファイルの viewBox を見直しました。svgタグには、どの範囲を表示するか指定する viewBox という属性があります。このviewBox のアスペクト比と実際に表示させる画像のアスペクト比を一致させることで、この問題に対応することができます。

アイコン

一部のアイコンにも、SVGフォントを使いました。IcoMoon などを使えば簡単に導入できて便利です。ただし、Androidでは要注意です。

通常、Webフォントをアイコンに使う場合、特定の文字コードをアイコンに割り当てます。Unicodeでは、外字エリアとして私用領域という名称で U+E000〜U+F8FF、U+000F0000〜U+000FFFFDなどが割り当てられているので、これを使うことが多い。前述の IcoMoon はこの外字エリアを使っていて、U+E001 から埋めていきます。

ところが、一部Andoroid(Softbank端末など)は、この外字エリアに独自のアイコンを定義してたりします。→ 携帯電話の絵文字

U+E001 は ソフトバンクの絵文字ともろに被っています。今回は時間が無かった & ドコモ向けなので、残念ながら対応は見送りましたが、IcoMoon などを使う場合、コードには十分気をつけてください。

メディアクエリ

SVG画像のスケールは上記の対応で可能になりました。次は、メディアクエリを使い、幅に応じて画像や文字の大きさを変更します。メディアクエリで端末サイズを判定するには主に二つの方法があります。

  1. min-width, max-width で判定する。
  2. min-device-width, max-device-width で判定する。

1つ目の方法は、windowの幅で判定するのに対して、2つめは、端末の幅で判定します。PCブラウザから見るとどちらも同じように動きますが、スマートデバイスでみると大きく異なります。特に回転した場合に違いが出ます。デバイスの向きを横に回転したとき、1の方法で横幅の変更を検知し、レイアウトを変更できます。しかし、2つめの方法では端末自体の幅は固定のため、回転しても変更されません。そのため、向きを判定するためには orientation:portrait / orientation:landscape を組み合わせて使用します。 なお、この orientation は厳密には向きを見ているのでは無く、縦長か横長なのかを見ているだけです。なので、PCブラウザで縦長/横長を変更しても反映されます。

どっちの判定を使えば良いか、というと前者をオススメします。というのも、これまた一部のAndroid標準ブラウザでは、device-width を正しく認識しません。例えば、Galaxy Nexus, S2, S3 なんかでは、window.innerWidth は 360px なのに、メディアクエリの device-width は 320px として認識されます。細かい調整をする場合、正しく認識されないのは致命的です。また、device-widthを使用する場合は orientation も同時に指定しないと意味が無いため、記述も長くなり、お勧めしません。

まとめ

Webアプリでも表現が幅がだいぶ広がってきました。今後、WebRTC や WebGLとか使えるようになったらもっと凄いものができるようになると思います。まだ Android Chromeでもデフォルトはオフ、Safariは全然うごかない(今日発表の iOS7 で対応がでるかも!?)

現状では「かきまる」のようなシンプルなものでも、意外と苦労しています。スマホ向けWebアプリで、なめらかなUIや、複数端末への対応は骨が折れます。特にAndroidは本当にツライのでGoogleさんマジでなんとかしてください(切実

HTML5機能を色々使ったスマホ向けWebアプリを作ってみた話:前編

昨日、ドコモがやっているWebアプリコンテストのノミネート作品が一般公開されました。

私も、デザイナさんと協同して作品を一つ出しました!面白かったら下記ページの いいねをお願いします!

かきまる

スマートフォンを使いながらも、アナログな感触をもっと楽しんで欲しい。そういう想いで考えられた、デジタルとアナログを繋ぐお絵かきWebアプリです。

.

子供達が家の中でスマホだけをいじって遊ぶのでは無く、実際のリアルな紙と色鉛筆で本物の感触で遊んで欲しい。そのきっかけをスマホアプリで与えられないか?というモチベーションで作られました。そのあたりのコンセプトはデザイナさんのアイディア。私は主にその実装担当です。

そういう狙いはあるのですが、簡単に言ってしまえば紙芝居形式で動物の絵を表示して、それをお手本に絵を描く、という至ってシンプルなアプリです。

何も考えずに作れば、複数のWebページを遷移する単なるWebサイトとしても実現できます。しかし、それじゃあつまらないので、今回はそこに色々と実験的な要素を盛り込みました。

  • ページめくりをかわいくしたい。
  • ゲーム的な要素もちょっとあると面白い
  • ひらがなの書き順も同時に教えられたら。
  • かわいい文字で表示したい。
  • 音があると楽しい
  • スマホとタブレット両方対応。AndroidでもiPhoneでも。

これらの要望を HTML5 Webアプリとしてどうやって実現するか、が私の課題でした。今回のエントリーではその実装について簡単に解説します。要望があれば連載で詳しい内容も続けていければと思います。

カードをめくるように表現する

スワイプで次のスライドが表示されるようなアプリは良くあります。Webでもそういう表現が増えてきました。多用されているためやや食傷気味で、そのまま使うのは面白くない。ただ、スワイプジェスチャーでページをめくるという認識は一般にもだいぶ浸透しています。そこで、そのジェスチャーはそのままに、表現をすこし工夫しました。ページをカードに見立て、カードをめくるように次々と下のカードが出てくるような表現にしてみました。

スライドのライブラリは沢山あるのですが、カードのような表現ができるライブラリが見つからなかったため、一から自作しました。タッチイベントのドラッグを処理するには、jQuery プラグインの Hammer.js が便利です。それに CSS Animation を組み合わせて実現しています。

使ってみると、予想以上にカードページングはスマホと相性が良い気がします。後から気がついたのですが、スマホ用 Google Chrome のタブなどもカードアニメーションしていますね。このカードページングは jQuery プラグインとして作成したので、整理できたら公開してみたいと思います。

スロットゲーム

動物をしりとり形式で繋げよう、というアイディアは当初からあったのですが、最初の動物(文字)をどうやって決めようか、と悩みました。ここにゲーム要素を持ってくることにしました。ひらがながスロットのように表示され、タップすると少し遅れて止まります。このスロットもライブラリとして作成したので、公開できればしたいと思っています。スロットの向き、速さ、止まるタイミングなどを色々検討するため、各種設定値で変更できるようにも作りました。

このスロットの中身の文字は、実は普通のテキストです。当初、画像にしていたのですが後述のWebFontを使うことで、文字だけで表現することができました。

また、スロットのタップイベントにも一工夫が必要でした。実際に子供達に使ってもらうと、タップが反応しない、という問題が起きました。スロットを止めるという動作の特性上、狙って押すと結構力が入ってしまうんですね。そうすると、タッチがぶれるんです。ぶれると、タップイベントではなく、ドラッグイベントとして認識してしまうというのが原因でした。これは、以前 @IT にも書いた記事 タッチUXを実現する7つのポイント で、自分も指摘していました。自分で書いてるのに考慮漏れとは情けない。。。

このタップの問題も、前述の Hammer を使うと各種閾値を調整すれば解決できます。ついでに、アイコンやボタンなどについてももう一度当たり判定エリアや閾値を見直し、子供でもタップしやすいように調整しました。

ひらがなの書き順を表示する

ひらがなの書き順をアニメーションで表現するのには、少し悩みました。一番実装が簡単なのは、各コマを用意してcanvasにパラパラアニメのように順次描いていく方法です。しかし、それでは文字が増えたときに大変です。そこで次に考えたのが、SVGのモーフィングを使うことです。Raphaël を使えば、割と簡単に実現できます。しかし結果からいうと、これはうまくいきませんでした。モーフィングの補完をちゃんと指定すればいいのですが、全文字でこれを何コマか設定するのはとてもやってられません。

そこで、ひらがなをグリフではなく、単1のパスの組み合わせとして作ることにしました。それをSVGのパスで表現し、書き順でパス要素を上から並べます。「あ」ならば、1つめの要素が横1本のパス、2つめの要素が縦1本のパス、3つの目の要素が、ぐるっとまわるパス、というような感じです。先ほどの Raphaël を使えば、長さを指定して、パスのサブパスを取得することができます。これを使用して、各パスのサブパスを段々伸ばしながら描画しています。

この文字データを作る際に、どうせならあ段だけじゃなくてひらがな全文字作ろう!といってデザイナさんが頑張って作ってくれました!当初50文字だけかと思ってたら、濁音とか半濁音とか、小さいかなとか意外とあって大変そうでした^^; ここでかなデータをSVGで全部作ったので、これを元に SVG Fontをつくり、後編の WebFontへと繋がります。



簡単に説明するつもりが、予定以上に長くなってしまいました。
ということで、続きは後編で!

→ 後編書きました! HTML5機能を色々使ったスマホ向けWebアプリを作ってみた話:後編

Safari/Mobile Safari(iOS) にクロージャを使った計算で桁溢れするバグ

前回のエントリ JavaScriptのfor/forEach/jQuery.each/Ext.each のパフォーマンスを計ってみた。の検証中に発見したバグの話し。

Safari 6.0.4 (8536.29.13) 及び、Mobile Safari(iOS 6.1.4)では、クロージャを使った計算では、桁溢れする可能性があります。前回検証に使ったスクリプト http://jsfiddle.net/dsuket/DR33Q/ の、nativeForEach 関数などがその対象です。

    var nativeForEach = function() {
        var sum = 0;
        list.forEach(function(item){
            sum += item;
        });
        return sum;
    }

上記スクリプトをループ回数 1,000,000回以上にした場合、桁あふれが起きて計算結果が正しくでません。

WebKitのバグレポートにもありました。
Bug 104967 - Summary: javascript integer overflow

どうやらJITコンパイラの最適化のバグのようです。JavaScriptの仕様的には Number は IEEE754標準の64ビット浮動小数点数形式で、表現可能な最大値は±1.7976931348623157×10308 です。しかし、パフォーマンスアップのため小さい数の場合はJITが int32として計算するようです。大きくなるとint64となるはずなのですが、クロージャを使った場合うまくいかないという問題です。そのため32bitの範囲(+2147483647〜-2147483647)を超えると桁溢れを起こします。

なお、ChromeFireFoxではもちろん期待通り動作します。SafariでもWebインスペクタを開いていると、この現象は起きない(ちゃんと計算できている)ためデバッグ中は気がつきにくい。。。

上記のバグレポートでは半年も前に RESOLVED FIXED になっているのですが、まだ Safari/Mobile Safariには反映されていません。普段 Safari をメインに使うことはあまりありませんが、iOSの場合は注意が必要ですね。

実はこのような話は昔からあるようです。

JavaScriptのfor/forEach/jQuery.each/Ext.each のパフォーマンスを計ってみた。

先日、Sencha Touch 2 ソースコード読書会 第2回@東京でナビゲータを務めさせて頂きました。その時の様子は以下などに。

この中で、Array.forEachと Ext.each のどちらがどのくらい速いのか、というのが話題に出ました。そういえば昔、JS ArrayのforEach, filter, map の速度を調べてみた。というエントリで調べました。あれから1年くらい立ってるんだけど、最近はどうなのかまた調べてみました。

jsfiddle を使って、生のfor文、Array.forEach、Ext.each、jQuery.each の比較ができるようにしました。
http://jsfiddle.net/dsuket/DR33Q/

配列に0から指定数まで数字を入れて、ループでそれらを足し合わせる計算時間を測定しています。私の環境(iMac Early 2009 )だと 1000万回くらいから差が出てきました。以下は OS X 10.8.3/Chromeで、各五回実行した最小の測定結果。単位は ミリ秒。

Chrome 27.0.1453.93

回数 for forEach jQuery.each Ext.each
100万 0 7 11 10
1000万 2 106 156 149
1億 20 762 1284 1189

for文が圧倒的に速いですね。jQuery や Ext の each は ネイティブの forEach に比べると 60%程遅いようです。あと、なぜか jQuery より Ext のほうが何回やってもちょっとだけ速い!

FireFoxSafariでもやってみました。

FireFox 21.0

回数 for forEach jQuery.each Ext.each
100万 0 2 4 2
1000万 3 15 30 15
1億 51 138 322 141

Safari 6.0.4 (8536.29.13)

回数 for forEach jQuery.each Ext.each
100万 6 27 22 25
1000万 68 265 256 257
1億 680 2915 2460 2507

FireFox 速い!forEach がかなり速いですね。Ext.eachも負けないくらい速いです。それに比べ、Safari はループが遅い上に、初期化もすごく時間かかりました。さらに、Safariにはとんでもないバグがあることがわかりました。。

その話はまた次のエントリで。
→ 書きました! JavaScriptのfor/forEach/jQuery.each/Ext.each のパフォーマンスを計ってみた。

gruntで更新されたファイルを対象にタスクを動かす方法

JavaScript のビルドツール grunt が熱いということで使い始めました。

Sencha Touchの場合は senchaコマンドでビルドできるのですが、それ以外のプロジェクトでリリースビルドを作るのに、sassのコンパイル、cssのminify、jsの結合とminify、そしてリリースディレクトリへのコピーなどを一括で行うために使っています。

便利なんだけど、毎回全ビルドが走るのがどうにも非効率だなぁ、と感じていました。更新されたファイルだけ対象にしてくれればいいのに。というかantとかrakeとか他のビルドツールには普通あるでしょ!

同じような事を思っている人は当然いるわけで、gruntに提案されています。しかしまだ取り込まれていないようです。→ Provide conditional support for file inclusion

ここでも言われてますが、gruntのタスクに定義するファイルには filter オプションというのがあって、タスク実行時にsrcファイルを任意の関数でフィルタリングすることができます。これを使えば、更新されたファイルだけ対象することができそうです。

と、いうわけで作ってみた。dsuket/Gruntfile.js

3つのフィルタ関数を作りました。

  • newer
    • srcがdestよりも新しければ true を返す。
  • expandNewer
    • destにディレクトリを指定して、srcの各ファイルと比較する。
  • newerAny
    • srcの何れかがdescよりも新しければ true を返す。

grunt のカスタムフィルタ関数は、タスク実行前に srcファイルを引数に呼ばれます。true を返せばそのファイルを対象とし、falseならば対象としません。Custom Filter Function

フィルタ関数の引数には srcしか渡されないため、destファイルがわかりません。そのため、フィルタ関数を返す関数を作成し、その引数に dest ファイルを指定するようにしました。やや冗長ですが仕方ない。

悩ましいのが、newerAny。フィルタ関数は1つのsrcファイル毎に呼び出されるため、予め全てのsrcがわかりません。そこで第2引数にsrcの定義を取るようにしました。配列でも定義できますが、文字列で与えた場合、その変数の値を取ります。(あんまりいけてない)

大体使えるようになりましたが、まだ問題はいくつかあります。

テンプレートが使えない。

テンプレートの展開は grunt.initConfig() の中で行われるので、newerのターゲットでテンプレートを使っても展開されません。

watch で newerAny が更新されない

最初のsrcファイルのフィルタ実行時にしか判定しないため、watchで起動したときは更新されません。毎回チェックするようにすればいいんですが、今のところwatchはあまり使っていないのでペンディング。

newerAny で expand が使えない。

srcファイルの展開は実行時行われるため、定義時にsrcの一覧が取得できません。


と、まぁ幾つか細かい問題がありますが、このフィルタの最大の問題点は、uglify で失敗するということです。

grunt-contrib-uglify タスクが、srcファイルが無い場合エラーで止まる問題があります。filterの結果、更新ファイルがなければ src は空になるのですが、その場合以降のタスクが止まってしまいます。他のタスクはsrcが空でも動くようになっているので、grunt-contrib-uglifyのバグだと思います。一応修正版を github にアップしました。(pull requestも出しておいた)
https://github.com/dsuket/grunt-contrib-uglify


参考:

iOS/Android で HTML5 の audio/video を任意のタイミングで再生する方法

HTML5 で追加された audio/video 属性によって、プラグインレスで動画や音声の再生が可能になったことはよく知られていますが、モバイル(スマホ)で、その再生タイミングの制約が厳しいことは案外知られていません。これは以前、当Blogでも HTML5 x Touch UI の UXを考える(補足) の User action event restrictions でもチラッと書きました。またしてもこの制約に苦しめられながら、なんとか解決策を見いだしたので、GW最中にも関わらず久しぶりのエントリーです。

まず、本家 Apple のドキュメントにも以下のようにあります。
iOS-Specific Considerations

In Safari on iOS (for all devices, including iPad), where the user may be on a cellular network and be charged per data unit, preload and autoplay are disabled. No data is loaded until the user initiates it. This means the JavaScript play() and load() methods are also inactive until the user initiates playback, unless the play() or load() method is triggered by user action. In other words, a user-initiated Play button works, but an onLoad="play()" event does not.

(超意訳)

iOS の Safari では、ユーザーが携帯網で重量課金されるかもしれないから、preload と autoplay は効かないよ。ユーザーが何かアクションしないと、JavaScript での play() や load() もダメ。つまり、再生ボタンを押して play() を呼べるけど、onload で play() しても無効だから。

このユーザーアクションというのが結構厳しい。touch/mouseイベントの直接のハンドラ内でしか効きません。setTimeoutなどのタイマーを挟むともうアウトです。

例えば、以下のようなコードは再生できます。http://jsfiddle.net/dsuket/jTh3v/

var playBtn = document.getElementById('play');
playBtn.addEventListener('click', function() {
    audio.play();
}, false);

しかし、これにタイマー挟むと再生されません。 http://jsfiddle.net/dsuket/Ny9Xz/ (PCブラウザでは再生できます)

var playBtn = document.getElementById('play');
playBtn.addEventListener('click', function() {
    setTimeout(function(){
        audio.play();
    }, 2000);
}, false);

これは setTimeout だけじゃなく、例えば transitionEnd などのアニメーション関連のイベントなどもダメです。そうすると、アニメーション終了時のタイミングで音声を再生したい場合に非常に困る。

そこで、以下のようにすると再生できます。http://jsfiddle.net/dsuket/rWpM7/

var playBtn = document.getElementById('play');
playBtn.addEventListener('click', function() {
    audio.load();
    setTimeout(function(){
        audio.play();
    }, 2000);
}, false);

ポイントは、setTimeout の前に load() することです。play() メソッドは内部で自動的に load が呼ばれるのですが、タイマーハンドラの中ではこの load が抑止されているようです。そこで、ユーザーイベントが発生した段階ですぐに load() だけ呼び出し、後は任意のタイミングで play() すればOK!

なお、ドキュメントonload のタイミングで自動再生したい、というのはやはりできません。そこで、少し画面フローを工夫して、最初にスプラッシュ画面を用意し、タップしてもらうようにします。そのタップイベントハンドラの中で、オーディオファイルをロードします。HTML5 Webアプリでよく使われる Single Page Applicationの場合、ここで必要なオーディオファイルを全部ロードしてしまうと良さそうです。そうすれば後は好きなタイミングで音声を再生できます。

audioについての説明でしたが、videoも同様の問題があります。検証できてませんが、同じようにすれば対応できそうです。

ただ、audio/videoの自動再生や再生タイミングはOSバージョンでコロコロかわります。そもそも iOS4.1、Android 2.3 までは setTimeout の中でも再生できました。それが iOS 5、Andoroid 4 あたりから再生できなくなりました。バージョンが違えばこの load を切り離す方法もまた使えなくなるかもしれません。十分検証には気をつけてください。


参考: