HTML5Experts.jp https://html5experts.jp 日本に、もっとエキスパートを。 Mon, 15 Aug 2016 00:35:15 +0000 ja hourly 1 http://wordpress.org/?v=4.2.9 Chrome 52新機能、最新macOS「Sierra」パブリックベータ公開─2016年7月のブラウザ関連ニュース https://html5experts.jp/myakura/20202/ https://html5experts.jp/myakura/20202/#comments Mon, 15 Aug 2016 00:26:44 +0000 https://html5experts.jp/?p=20202 連載: WEB標準化動向 (15)

2016年7月のブラウザ関連ニュースは、7月20日にリリースされたChrome 52、macOSの最新バージョン「Sierra」のパブリックベータ公開についてお伝えします!

Chrome 52リリース

7月20日にChrome 52がリリースされました。

Mac版は見た目がすっきりしました。

スクリーンショット:Chrome 51とChrome 52のウインドウを並べたもの

グラデーションや丸みがなくなりすっきりした。タブも少し大きくなった

新しい見た目は、モバイル版ChromeやChrome OSと同じものになっています。細かいところについては、GoogleでChromeのデザインをしているSebastien Gabrielが自身のサイトやMediumでまとめています。

新しい機能の紹介をしましょう。今回はパフォーマンス向上に貢献しそうなものが多く入っています。

Service Worker/Fetch API関連で、レスポンスを ReadableStream で作成し返せるようになりました。

うまく使えば段階的にコンテンツを表示できるので、体感速度が上がります。

CSSではCSS Containmentというものが実装されました。指定した要素について、レイアウトや描画のスコープを設けられる仕組みです。

基本的にレイアウトや描画はドキュメント全体がスコープになるため、一部のみ変更する際にはドキュメント全体が対象となる無駄な処理が走ります。それを抑制できるので、とくに複雑なページで使うと恩恵が大きいでしょう。

また、新機能ではありませんが、外部スタイルシートが起こすレンダリングの挙動が変更されました。

これまでは外部スタイルシートをパーザが発見した場合、それが読み込まれるまで描画をブロックしていたのですが、すでにパーザが処理したDOMの描画も止めていたため、段階的なレンダリングができませんでした。新しい挙動は外部スタイルシートが見つかった箇所以降のみレンダリングのブロックが発生します。段階的なレンダリングにやさしくなります。

この挙動とHTTP/2を組みあわせ、必要なときに必要なCSSを読みこむやり方が検討されています。

パフォーマンス計測のためのPerformance Observerも実装されました。Navigation Timingをはじめとした各種パフォーマンス情報をObserverで取得する新しいAPIです。ポーリングなどをしなくてもよくなりますし、必要なパフォーマンス情報を絞れるので、扱いやすくなるかと思います。

パフォーマンス関連以外の機能もちょこちょこあります。

  • Canvas APIに ctx.filter が追加され、フィルタを指定できるようになりました
  • CSS Color Module Level 4で定義されている #rrggbbaa 記法が使えるようになりました
  • V8も更新され、ES7のexponentiation operator(**)が実装されました

macOS Sierraパブリックベータ公開

OS Xから名前が変わったmacOSの最新バージョン、Sierraのパブリックベータが公開されました。

Safari 10が入っていますが、開発者向けのドキュメントで取り上げられていない機能も一部入っています。

ひとつはCSSアニメーションのの spring() です。「ばね」と名のつくように、ぼよんぼよんと動くアニメーションを簡単に使えるものです。Appleが内部で必要として試験的に実装されたもので、後にAppleより標準化も提案されています。

頑張れば既存の仕組みでも実現できるかもしれませんが、専用の関数が用意されているとだいぶ楽そうです。 また、タイミング関数を自作できる仕組みもほしいという話もコミュニティから出ています。

動きもコンポーネント的に定義できれば、ライブラリとしての提供もしやすそうです。

もうひとつは、初期のベータだけですが、WebPがOSレベルでサポートされていました。

しかし、後のベータでは削除されベータに含める意図がなかったことも明らかになっています。Appleが実装するまでWebPに興味があったというのが、うれしいニュースでしょうか。

]]>
https://html5experts.jp/myakura/20202/feed/ 0
Microsoft EdgeのUI機能が強化!ーWindows 10 Anniversary Updateにおける新機能一挙解説 https://html5experts.jp/osamum_ms/20244/ https://html5experts.jp/osamum_ms/20244/#comments Wed, 03 Aug 2016 01:09:34 +0000 https://html5experts.jp/?p=20244 Windows 10 が公開されてちょうど一年、Windows 10 の無償アップグレード期間が終了したのと入れ替わるように本日 (2016/8/3 日本時間) Windows 10 Anniversary Update が公開されました。

この Windows 10 Anniversary Update では、Windows 10 が発表された際の様々なビジョンのいくつかが実装され、さらにその当時は想像だにできなかった  Windows Subsystem for Linux (WSL) といったようなまったく新しい機能も搭載されています。

Windows 10 から搭載された新しい Web ブラウザーである Edge にも、当初の計画にあった新しい拡張 (エクステンション)モデルの実装や、Windows フィードバック、Developer FeedBack (旧 Edge Suggestion Box) に寄せられたフィードバックや提案をもとにした新しい機能が実装されています。

今回の記事では Windows 10 Anniversary Update の新機能、とくにユーザーが直接対話するデスクトップ UI の新機能と、新しく追加された Edge 関連のグループポリシーの設定について紹介します。

Microsoft Edge のデスクトップ UI 機能の強化

Windows 10 がリリースされてからこれまで、Edge にもたゆまず機能強化が行われてきましたが、その内容はパフォーマンスの向上であったり、サポートする API の数を増やすであるとか、どちらかというと開発者向けのものが多かった印象があります。

これら機能は UI を持たないため、開発者以外の多くのユーザーは Edge の機能向上の進捗を肌感として感じれなかったかもしれません。

しかし、Windows 10 Anniversary Update での Edge では、誰しもがアクセス可能なデスクトップ UI に複数の新機能を搭載しています。

この機能は、おおまかに分類すると以下の 4 つに別けられます。

  1. ナビゲーション
  2. お気に入り
  3. ダウンロード
  4. コンテキストメニュー

以降は、上記 4 つの項目について、具体的にどのどのような機能が追加されたのかを紹介していきます。

ナビゲーション

Web ブラウジングにおける “ナビゲーション” はページを遷移させるための操作や、その動作そのものを指します。

このナビゲーション関連では以下の機能が追加されています。

右クリックでのナビゲーション履歴表示

Edge のナビゲーションバーの左側にある 戻るボタン/進むボタン上で、マウスの右ボタンをクリックすることで、それまでの履歴と現在の位置(履歴内のどこをブラウズしているか)が表示され、クリックすることでそのページに遷移できるようになりました。

従来の 戻る/進む ボタンでは、ブラウザーを起動してからの履歴に対し、ひとつずつしか前後に遷移できませんでしたが、この機能を使用すると履歴の任意の位置に直接移動できます。

context_history


 

貼り付けて移動/検索

クリップボードにコピーした URL のページを Edge で表示させる場合、ナビゲーションバーに一度 URL を貼り付けてからキーボードの [ Enter] キーを押下する必要がありました。

また同様に、クリップボードにコピーしたキーワードで検索を行う場合には、 ナビゲーションバーに一度 キーワードを貼り付けてからキーボードの [ Enter] キーを押下する必要がありました。

Windows 10 Anniversary Update からの Edge では、ナビゲーションバー上でマウスの右ボタンをクリックした際に表示されるコンテキストメニューに [貼り付けて移動] もしくは [貼り付けて検索] というメニューが追加されており、このメニューを選択することで [Enter] キーを押下しなくてもクリップボードにある URL への移動、あるいはクリップボードにあるキーワードで検索を行うことができます。なお、「移動」か「検索」かは、クリップボードの内容により自動的に判断されます。

paste_and_go
(クリップボードの中身が URL の場合)


 

paste_and_find
(クリップボードの中身が URL ではない場合)


 

スワイプによるナビゲーション

スワイプによるナビゲーションに対応しました。

Windows Phone や タッチ対応のモニタの PC で Edge を使用する場合、画面を左/右にスワイプすることで一度遷移した URL について遷移することができます。

左にスワイプするとひとつ前に戻り、右にスワイプするとひとつ進みます。

お気に入り

Edge において「お気に入り」 (favorite) はユーザーが任意の URL を Web ブラウザーに登録しておく機能です。

お気に入り関連では以下の機能が追加されています。

ピン留め

ピン留めは Internet Explorer 8 からサポートされた機能で、当時はピン留め対応した Web サイトのショートカット アイコンとナビゲーションメニューを Windows のタスクバーに登録することができました。

Windows 10 Anniversary Update で提供される Edge には、ショートカットアイコンの登録場所や提供される機能は異なりますが、ピン留め機能が復活しています。

また、Web サイト側での特別な設定は不要となっています。

Edge のピン留め機能を使用するには、任意のページを表示したページのタブの上で右クリックし、表示されたコンテキストメニューから [ピン留めする] メニューを選択します。

Pinned tab

これによりタブの左端にページがピン留めされ、Edge が起動されると同時にページがロードされるようになります。

image


 

[お気に入り] メニューのツリー表示と並べ替え機能

これまで Edge の [お気に入り] メニューでは、同一階層にあるリンクのみが一覧で表示されていましたが、Windows 10 Anniversary Update で提供される Edge ではツリー表示されるようになりました。

favorit_tree

また、名前で並べ替えも出来るようになっています。

name_sort


 

[お気に入り] のインポート元の表示

Edge では他の Web ブラウザーから [お気に入り] をインポートできますが、インポート元の Web ブラウザーの名前がついたフォルダが作成され、どの Web ブラウザーからインポートされたのかわかるようになりました。

Inport


 

お気に入りバーのメニュー追加

お気に入りバー上にショートカットの表示で、アイコンだけを表示できる設定が追加されました。

favorite_setting


また、これまでお気に入りバーにはコンテキストメニューは設定されていませんでしたが、今回のアップデートで [新しいフォルダの作成] と [アイコンのみ表示する] メニューが追加されました。

FavoritesBar

ダウンロード

Edge のダウンロード機能については、保存場所や保存のさいのアクションについて、以下のような機能が追加されています。

保存先フォルダの指定

これまでの Edge では、既定のダウンロード先のフォルダは %user%\ダウンロード となっており、これを変更することは出来ませんでしたが、今回のアップデートでは [詳細設定] メニューからこれを変更できるようになりました。

image


 

ダウンロード先の指定

これまで、Edge ではファイルをダウンロードする際に保存先の指定を任意で行うことはできませんでしたが、今回のアップデートでは、ダウンロード時に表示されるダイアログボックスに [名前を付けて保存] ボタンが追加され、保存先の指定や、保存する際のファイル名の指定ができるようになりました。

dl_dialog


 

ダウンロード中に Edge 終了の際の警告

ファイルをダウンロード中に Edge をクローズ場合、警告メッセージが表示されるようになりました。

コンテキストメニューへの機能追加と変更

Web ページを表示する部分で、右クリックした際に表示されるコンテキストメニューにも機能が追加されています。

Cortana との連携

Edge に Cortana との連携機能が実装されました。

文字列、もしくは画像を選択し、マウスの右クリックメニューから [Cortana に質問] を選択すると、選択された内容に関連する情報を判断して検索を行い結果を列挙します。

Cortana


 

たとえば、以下のように画像を問い合わせた場合、Cortana は関連する情報と類似する画像を調査して列挙します。

image

 

開発者用メニューの表示タイミング

これまでの Edge では、表示されている Web ページ上での右クリックメニューに [要素の検査] と [ソースの表示] という開発者向けのメニューが、初期状態から表示されるようになっていました。

今回のアップデートでは、初期状態ではこれら開発者向けのメニューは表示されず、F12 開発者ツールが起動されてからはじめて表示されるようになっています。

 

ここまで Windows 10 Anniversary Update で追加される Edge の新機能について紹介してきました。

Edge の新機能については、かねてからのロードマップと、ユーザーから寄せられたフィードバックにもとづき実装しています。

しかしながら、期待していた機能が実装されていなかった、ということもあるでしょう。そういった場合には Edge に拡張インストールし、昨日を追加することでご要望を満たすことができるかもしれません。

拡張 (エクステンション) による Edge の機能強化

Windows 10 Anniversary Update の Edge では、拡張がサポートされました。

拡張とは、外部で作られたプログラムで、Edge にプラグインすることで Edge の機能を文字どおり拡張します。

Edge の拡張は Windows ストアから入手することができ、すでに Ad ブロックやマウスジェスチャー、翻訳等、さまざまな機能を提供する拡張が用意されています。

拡張の入手と Edge へのインストール

拡張の具体的な入手とインストール方法は以下のとおりです。

  1. Edge のツールバー上の  […] アイコンをクリックして [拡張機能] メニューをクリックします。
    image

     
  2. [拡張]パネルが表示されるので [ストアから拡張機能を取得する] リンクをクリックします。
    image

     
  3. Windows ストアの 「Microsoft Edge の拡張機能」 画面が開くので任意の拡張のアイコンをクリックします。
    ExtensionList

     
  4. 選択した拡張の説明画面に遷移するので [無料] 、もしくは金額の書かれた(※有料の拡張の場合) ボタンをクリックします。
    image

以上の手順で拡張のダウンロードとインストールが行われます。

拡張の設定とアンインストール

Edge にインストールした拡張のアンインストールやオプション設定は、Edge の [拡張機能] メニューで行います。

具体的な手順は以下のとおりです。

  1. Edge のツールバー上の  […] アイコンをクリックして [拡張機能] メニューをクリックします。
  2. [拡張]パネルが表示され、インストール済の拡張のリストが表示されるので、任意の拡張をクリックします。
  3. 拡張の詳細設定画面が表示されるので以下から目的の操作を行います。
    • 拡張を有効、もしくは無効に –  トグルボタンを操作
    • 拡張のオプション設定を行う – [オプション] ボタンをクリック
    • 拡張をアンインストールする – [アンインストール] ボタンをクリック
    image


 

Edge への拡張のインストール/アンインストール方法は以上です。

Edge 用の拡張を自分で作成する方法については以下の記事で紹介していますので、興味のある方はぜひご覧ください。

 

Windows 10 Anniversary Update で追加される Edge のグループポリシー設定

Windows 10 Anniversary Update では Edge のグループポリシーで設定できる内容も追加が行われています。

グループポリシーとは、IT 管理者が管理対象の使用者に対してさまざまな設定を一括で行うための機能です。

従来から Edge では、以下の内容について IT 管理者がグループポリシーを用いて Windows ドメイン ユーザーの使用する Edge を管理することができます。

設定 説明
オートフィルを無効にする Edge の使用中にフォームのフィールドにオートフィルで自動入力できるかどうか
開発者ツールを無効にする F12開発者ツールの使用を許可するかどうか
トラッキング拒否ヘッダーの送信を従業員に許可する トラッキング情報が要求される Web サイトに従業員がトラッキング拒否ヘッダーを送信を許可するかどうか
InPrivate ブラウズを無効にする InPrivate ブラウズの使用の有無
パスワードマネージャーを無効にする パスワードマネージャーを使用してパスワードのローカル保存を許可するかどうか
ポップアップブロックを無効にする ポップアップブロック機能を使用可とするかどうか
アドレスバーの検索候補を無効にする アドレスバーに検索候補を表示するかどうか
SmartScreen フィルターを無効にする SmartScreen フィルターを有効にするかどうか
Open a new tab with an empty tab ([新しいタブ]ページでのWebコンテンツの許可) 新しいタブを開いたときに表示するページの種類を構成
Cookie を構成する Cookie の扱いを構成
エンタープライズモードサイト一覧を構成する エンタープライズ モードとエンタープライズ モード サイト一覧を使用するかどうかを構成
お気に入りを構成する ユーザーに表示される既定のお気に入りを構成できます
WebRTC による LocalHost IP アドレスの共有をしない WebRTC プロトコルを使用した通話中にユーザーの LocalHost IP アドレスが表示されるかどうかを指定
企業のホームページを構成する ドメインに参加しているデバイス用に企業のホーム ページを構成
SmartScreen フィルター機能の警告の上書きを許可しない 有害である可能性のある Web サイトに関する SmartScreen フィルター機能の警告を従業員が上書きできるかどうかを指定
確認されていないファイルに関するSmartScreen フィルター機能の警告の上書きを許可しない 確認されていないファイルのダウンロードに関する SmartScreen フィルター機能の警告をユーザーが上書きできるかどうかを指定
すべてのイントラネットサイトを Internet Explorer 11 に送る イントラネット サイトを Internet Explorer 11 で表示するかどうかを指定

 

今回のアップデートでは、新たに以下の内容が制御可能となりました。

設定 説明
Microsoft Edge で about:flags ページへのアクセスを禁止する ユーザーが about:flags ページにアクセスできるかどうかを指定
拡張機能の許可 ユーザーが拡張機能を読み込めるかどうかを許可
Interner Explorer でサイトを開くときのメッセージ表示 Internet Explorer 11 でサイトが開かれたことを示す追加ページを Microsoft Edge で従業員に表示するのかどうかを指定

これらグループポリシーの新しい設定を使用することで、IT 管理者は今回のアップデートで追加された Edge の新機能の使用の有無を管理することかできます。

Microsoft Edge の更新履歴について

Windows 10 のビルド単位での、より詳しい更新履歴については、以下のページで確認することができます。

Microsoft Edge がサポートする API については以下をご参照ください。

まとめ

これまでの Edge では、Web コンテンツの閲覧中 Internet Explorer 11 と比較して機能が少ないと感じる場面が多々あったかもしれません。しかし、そういった不満は Windows 10 Anniversary Update によってずいぶんと解決されていると思います。

また、それでも足らないと感じる機能については、拡張を追加することである程度解消できることでしょう。

Edge を使ってみて他の Web ブラウザーを使うようになってしまった人も、Edge をまだ使用したことがない人もぜひこの機会に Edge をお試しくださいませ。

]]>
https://html5experts.jp/osamum_ms/20244/feed/ 0
シグナリングを拡張して、複数人で通信してみよう ーWebRTC入門2016 https://html5experts.jp/mganeko/20112/ https://html5experts.jp/mganeko/20112/#comments Mon, 01 Aug 2016 00:00:18 +0000 https://html5experts.jp/?p=20112 連載: WebRTC入門2016 (4)

こんにちは! 2014年に連載した「WebRTCを使ってみよう!」シリーズのアップデート記事も4回目となり、佳境に入りました。前回の1対1の通信をベースに、今回はより実用的なビデオチャットを目指して複数人で通信可能なように拡張してみましょう。

複数人、複数会議室を目指して

前回作ったのは、1つのシグナリングサーバーに対して、同時に1ペアだけが利用できる仕組みでした。これを複数人で、複数会議室で利用できるようにしていきましょう。こちらの図の左のBeforeの状態から、右のAfterの状態を実現します。

rtc_11_to_nn

複数人で通信するためには

WebRTCはPeer-to-Peer (P2P) で通信する仕組みです。なので複数人と通信するためには、複数のRTCPeerConnectionを用意する必要があります。図にするとこんな感じです。

PeerConnection_multi

また、P2P通信を開始するためにSDP(Offer/Answer)を交換する必要がありますが、それぞれの相手ごとに生成、交換しなければなりません。

PeerConnection_sdp_multi

P2Pなので当然かもしれませんが、私は最初に複数の相手と通信しようとして混乱してしまいました。この考え方を踏まえていれば、あとは力技になります。

シグナリングサーバーの対応

シグナリングサーバーは前回の1対1の時と同じく、Node.jsを使って用意しましょう。複数会議室を実現するのに便利なため、今回はwsモジュールではなく、より高機能のsocket.ioを使います。Node.jsのインストールは終わっていると思うので、コマンドプロンプト/ターミナルから、次のコマンドを実行してください。 ※必要に応じて、sudoなどをご利用ください。

npm install socket.io

2014年にはsocket.ioはv0.9でしが、今回はv1.4.xになっています。

シグナリングサーバーのコードは前回の1対1と同様にメッセージを中継するのが役目ですが、接続してきたクライアントがルーム(会議室)に入室要求を送ってきたら、socket.ioのサーバ側でそのルームに join() してあげます。

// ---- multi room ----
    socket.on('enter', function(roomname) {
      socket.join(roomname);
      console.log('id=' + socket.id + ' enter room=' + roomname);
      setRoomname(roomname);
    });

    function setRoomname(room) {
      socket.roomname = room;
    }

クライアントからのメッセージ送信には、次の2つのパターンがあります。

  • ルーム内の他のメンバー全員(接続している他のクライアントすべて)に送る
  • 特定のメンバー(特定のクライアント)だけに送る

シグナリングサーバーでは、送信先が指定されていればその相手だけに、指定されていなければルーム内の全員(送信者以外)にメッセージを送ります。 ※その際に、送信元を特定できるID(socket.ioが管理しているID)を追加しています。

socket.on('message', function(message) {
        message.from = socket.id; // 送信元のIDをメッセージに追加

        // get send target
        var target = message.sendto;
        if (target) { // 特定の相手に送る場合
          socket.to(target).emit('message', message); 
          return;
        }

        // broadcast in room
        emitMessage('message', message);
    });

    // ルーム内の全員に送る場合
    function emitMessage(type, message) {
      // ----- multi room ----
      var roomname = getRoomname();

      if (roomname) {
        // ルーム内に送る
        socket.broadcast.to(roomname).emit(type, message);
      }
      else {
        // ルーム未入室の場合は、全体に送る
        socket.broadcast.emit(type, message);
      }
    }

サーバーの全体のソースは次の通りです。これを例えば signaling_room.js というファイル名で保存します。

"use strict";

var srv = require('http').Server();
var io = require('socket.io')(srv);
var port = 3002;
srv.listen(port);
console.log('signaling server started on port:' + port);

// This callback function is called every time a socket
// tries to connect to the server
io.on('connection', function(socket) {
    // ---- multi room ----
    socket.on('enter', function(roomname) {
      socket.join(roomname);
      console.log('id=' + socket.id + ' enter room=' + roomname);
      setRoomname(roomname);
    });

    function setRoomname(room) {
      socket.roomname = room;
    }

    function getRoomname() {
      var room = socket.roomname;
      return room;
    }

    function emitMessage(type, message) {
      // ----- multi room ----
      var roomname = getRoomname();

      if (roomname) {
        console.log('===== message broadcast to room -->' + roomname);
        socket.broadcast.to(roomname).emit(type, message);
      }
      else {
        console.log('===== message broadcast all');
        socket.broadcast.emit(type, message);
      }
    }

    // When a user send a SDP message
    // broadcast to all users in the room
    socket.on('message', function(message) {
        var date = new Date();
        message.from = socket.id;
        console.log(date + 'id=' + socket.id + ' Received Message: ' + JSON.stringify(message));

        // get send target
        var target = message.sendto;
        if (target) {
          console.log('===== message emit to -->' + target);
          socket.to(target).emit('message', message);
          return;
        }

        // broadcast in room
        emitMessage('message', message);
    });

    // When the user hangs up
    // broadcast bye signal to all users in the room
    socket.on('disconnect', function() {
        // close user connection
        console.log((new Date()) + ' Peer disconnected. id=' + socket.id);

        // --- emit ----
        emitMessage('user disconnected', {id: socket.id});

        // --- leave room --
        var roomname = getRoomname();
        if (roomname) {
          socket.leave(roomname);
        }
    });

});

コマンドプロンプト/ターミナルから、 次のように起動してください。(ファイル名は適宜置き換えてくださいね)

node signaling_room.js

クライアント側の拡張

次はクライアントとなるブラウザ側の処理を拡張していきます。

socket.io サーバーへの接続

今回はシグナリングサーバーを同じPCの3002番ポートで動かしていると想定します。HTMLファイルの先頭で、socket.ioのクライアント用のjsファイルを読み込みます。

<!doctype html>
<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <title>multi party</title>
 <script src="http://localhost:3002/socket.io/socket.io.js"></script>
</head>
... 省略 ...
</htm>

クライアントではsocket.ioのサーバー (localhost:3002) に接続します。このとき、ws://~ ではなく、http://~ となることがWebSocketを直接利用した場合と異なります。

// ----- use socket.io ---
  let port = 3002;
  let socket = io.connect('http://localhost:' + port + '/');
  socket.on('connect', function(evt) {
    // 接続したときの処理
  });

※実際のシグナリングサーバーの環境に合わせて、URLやポート番号は変更してください。

ルーム(会議室)への入室

クライアンではsocket.ioのサーバーに接続したら、希望のルームに入室を依頼します。ルーム名は今回はURLの後ろに ?部屋名 という形で指定することにしてみました。(お好きな方法で指定してください)

let room = getRoomName();
  socket.on('connect', function(evt) {
    console.log('socket.io connected. enter room=' + room );
    socket.emit('enter', room);
  });

  // -- room名を取得 --
  function getRoomName() { // たとえば、 URLに  ?roomname  とする
    let url = document.location.href;
    let args = url.split('?');
    if (args.length > 1) {
      let room = args[1];
      if (room != '') {
        return room;
      }
    }
    return '_testroom';
  }

複数通信の流れ

1対1の時は相手が1人しかいない前提だったので、ただちにOffer SDP / Answer SDPを送信していました。今回は相手が何人いるか分からない状況からスタートしますし、相手ごとに個々にOffer SDP / Answer SDPを送受信する必要があります。そこで、相手を確認するやりとりを追加しました。図にすると、次のような流れになります。

multi_callme_simple

もう少し細かく見ると、次のような処理を行っています。

  • 新たに通信を開始したい人(ブラウザA)が、通信開始の合図のルーム内にブロードキャストする(“call me”)
  • 受け取った人(ブラウザB、ブラウザC)は、Offer SDPを生成して、ブラウザAを宛先に指定して送ります
    • RTCPeerConnectionを生成
    • RTCPeerConnection.createOffer()でOffer SDPを生成
    • setLocalDescription()で覚える
    • ブラウザA宛てに送信
  • ブラウザAは、Offer SDPを受け取り、Answer SDPを生成してそれぞれの相手に送り返します
    • 相手ごとにRTCPeerConnectionを生成
    • それぞれ受け取ったOffer SDPをRTCPeerConnection.setRemoteDescription()で覚える
    • RTCPeerConnection.createAnswer()でAnswer SDPを生成
    • setLocalDescription()で覚える
    • それぞれの相手にAnswer SDPを返信
  • ブラウザB、ブラウザCは、Answer SDPを受け取る
    • 受け取ったAnswer SDPをRTCPeerConnection.setRemoteDescription()で覚える

multi_callme

ソースの修正:複数通信の準備

それでは、ブラウザ側のソースも手を入れていきましょう。まず、複数のRTCPeerConnectionを扱えるように用意します。

// ---- for multi party -----
  let peerConnections = [];
  const MAX_CONNECTION_COUNT = 3;

  // --- RTCPeerConnections ---
  function getConnectionCount() {
    return peerConnections.length;
  }

  function canConnectMore() {
    return (getConnectionCount() < MAX_CONNECTION_COUNT);
  }

  function isConnectedWith(id) {
    if (peerConnections[id])  {
      return true;
    }
    else {
      return false;
    }
  }

  function addConnection(id, peer) {
    peerConnections[id] = peer;
  }

  function getConnection(id) {
    let peer = peerConnections[id];
    return peer;
  }

  function deleteConnection(id) {
    delete peerConnections[id];
  }

相手の映像を表示するvideoタグも、動的に生成して複数管理できるようにします。

let remoteVideos = [];
  let container = document.getElementById('container');

  // --- video elements ---
  function addRemoteVideoElement(id) {
    let video = createVideoElement('remote_video_' + id);
    remoteVideos[id] = video;
    return video;
  }

  function getRemoteVideoElement(id) {
    let video = remoteVideos[id];
    return video;
  }

  function deleteRemoteVideoElement(id) {
    removeVideoElement('remote_video_' + id);
    delete remoteVideos[id];
  }

  function createVideoElement(elementId) {
    let video = document.createElement('video');
    video.width = '160';
    video.height = '120';
    video.id = elementId;

    video.style.border = 'solid black 1px';
    video.style.margin = '2px';

    container.appendChild(video);

    return video;
  }

  function removeVideoElement(elementId) {
    let video = document.getElementById(elementId);
    container.removeChild(video);
    return video;
  }

さらにRTCPeerConnectionの接続や、相手からのメディアストリーム、videoタグを連動して扱う処理も追加しておきましょう。

// --- video elements ---
  function attachVideo(id, stream) {
    let video = addRemoteVideoElement(id);
    playVideo(video, stream);
    video.volume = 1.0;
  }

  function detachVideo(id) {
    let video = getRemoteVideoElement(id);
    pauseVideo(video);
    deleteRemoteVideoElement(id);
  }
  
  function isRemoteVideoAttached(id) {
    if (remoteVideos[id]) {
      return true;
    }
    else {
      return false;
    }
  }

  // --- RTCPeerConnections ---
  function stopConnection(id) {
    detachVideo(id);

    if (isConnectedWith(id)) {
      let peer = getConnection(id);
      peer.close();
      deleteConnection(id);
    }
  }

  function stopAllConnection() {
    for (let id in peerConnections) {
      stopConnection(id);
    }
  }

ソースの修正:シグナリングの変更

WebSocket直接利用から、socket.ioの利用に変わったので、シグナリングも変更します。

// ----- use socket.io ---
  let port = 3002;
  let socket = io.connect('http://localhost:' + port + '/');
  let room = getRoomName();
  socket.on('connect', function(evt) {
    socket.emit('enter', room);
  });
  socket.on('message', function(message) {
    let fromId = message.from;

    if (message.type === 'offer') {
      // -- got offer ---
      let offer = new RTCSessionDescription(message);
      setOffer(fromId, offer);
    }
    else if (message.type === 'answer') {
      // --- got answer ---
      let answer = new RTCSessionDescription(message);
      setAnswer(fromId, answer);
    }
    else if (message.type === 'candidate') {
      // --- got ICE candidate ---
      let candidate = new RTCIceCandidate(message.ice);
      addIceCandidate(fromId, candidate);
    }
    else if (message.type === 'call me') {
      if (! isReadyToConnect()) {
        console.log('Not ready to connect, so ignore');
        return;
      }
      else if (! canConnectMore()) {
        console.warn('TOO MANY connections, so ignore');
      }

      if (isConnectedWith(fromId)) {
        // already connnected, so skip
        console.log('already connected, so ignore');
      }
      else {
        // connect new party
        makeOffer(fromId);
      }
    }
    else if (message.type === 'bye') {
      if (isConnectedWith(fromId)) {
        stopConnection(fromId);
      }
    }
  });
  socket.on('user disconnected', function(evt) {
    let id = evt.id;
    if (isConnectedWith(id)) {
      stopConnection(id);
    }
  });

  // --- broadcast message to all members in room
  function emitRoom(msg) {
    socket.emit('message', msg);
  }

  function emitTo(id, msg) {
    msg.sendto = id;
    socket.emit('message', msg);
  }

通信開始要求の”call me”や、切断要求の”bye”の処理を追加しています。また接続準備が整っていない場合(カメラやマイクを取得していない場合)や、すでに接続中の相手からの接続要求は無視するようにしています。

SDPやICE candidateのハンドリング

複数の相手とOffer/Answer SDPやICE candidateをやり取りするので、相手を意識した処理に拡張します。といっても違いは対応するRTCPeerConnectionのオブジェクトを生成したり取り出したりするところだけで、他は1対1の場合と同様です。すべてを掲載すると長いので全体はGitHubを参照してくただくとして、ここでは例を取り上げます。

Offer SDPの送信

新たに RTCPeerConnectionを生成し、Offer SDPの作成、送信を行います。

function makeOffer(id) {
    peerConnection = prepareNewConnection(id);
    addConnection(id, peerConnection);

    peerConnection.createOffer()
    .then(function (sessionDescription) {
      console.log('createOffer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICEの場合は、初期SDPを相手に送る -- 
      sendSdp(id, peerConnection.localDescription);

      // -- Vanilla ICEの場合には、まだSDPは送らない --
    }).catch(function(err) {
      console.error(err);
    });
  }

  function sendSdp(id, sessionDescription) {
    let message = { type: sessionDescription.type, sdp: sessionDescription.sdp };
    emitTo(id, message);
  }

Answer SDPを受け取った場合の処理

対応するRTCPeerConnectionを取り出し、Answer SDPを覚えさせます。

function setAnswer(id, sessionDescription) {
    let peerConnection = getConnection(id);
    if (! peerConnection) {
      console.error('peerConnection NOT exist!');
      return;
    }

    peerConnection.setRemoteDescription(sessionDescription)
    .then(function() {
      console.log('setRemoteDescription(answer) succsess in promise');
    }).catch(function(err) {
      console.error('setRemoteDescription(answer) ERROR: ', err);
    });
  }

ICE candidateのやりとりなども、同様に対応するRTCPeerConnectionのオブジェクトを取り出して行います。また、手動シグナリングで使っていたSDPをテキストエリアに表示する部分も取り除きました。

カメラ、マイクの取得

前回までは映像だけでしたが、今回はマイクの音声も取得してより実用的にしましょう。ただし1台のPCでやる場合はハウリングしてしまうので、ヘッドフォンを使ってください。

// start local video
  function startVideo() {
    getDeviceStream({video: true, audio: true})
    .then(function (stream) { // success
      localStream = stream;
      playVideo(localVideo, stream);
    }).catch(function (error) { // error
      console.error('getUserMedia error:', error);
      return;
    });
  }

getDeviceStream()は、新旧getUserMedia()をラップするために用意した関数です。これにaudio:trueの指定を渡してマイクも取得しています。

今回マイクの音声も取得するようにしたため、メディアストリームにはビデオとオーディオの2つのトラックが含まれます。このため、相手側のRTCPeerConnectionontrack()イベントが2回呼び出されますが、2回目は無視するように修正しました。(RTCPeerConnection.ontrack()は現在Firefoxのみがサポートしています)

function prepareNewConnection(id) {
    let pc_config = {"iceServers":[]};
    let peer = new RTCPeerConnection(pc_config);

    // --- on get remote stream ---
    if ('ontrack' in peer) {
      peer.ontrack = function(event) {
        let stream = event.streams[0];
        if (isRemoteVideoAttached(id)) {
          console.log('stream already attached, so ignore'); // <--- 同じ相手からの2回目以降のイベントは無視する
        }
        else {
          //playVideo(remoteVideo, stream);
          attachVideo(id, stream);
        }
      };
    }
    else {
      peer.onaddstream = function(event) {
        let stream = event.stream;
        console.log('-- peer.onaddstream() stream.id=' + stream.id);
        //playVideo(remoteVideo, stream);
        attachVideo(id, stream);
      };
    }

    // ... 省略 ....
  }

全体のソース

主要な部分は以上の通りですが、他にも細かい修正があります。全体のソースはGitHubでご覧ください。

接続してみよう

今回のサンプルでは4人まで同時に通信できるようにしてみました。それぞれブラウザを立ち上げて[Start Video]→[Connect]の順にボタンをクリックし接続してください。このように複数の相手と接続できるはずです。

multi_connected_4

※localhost以外に繋ぐときは、Chromeではカメラ/マイクの取得に失敗します。その場合はFirefoxをご利用ください。

次回は

今回のサンプルでは複数人で同時にビデオチャットができるようにしました。が、Chromeではhttp://~でgetUserMedia()が許可されていないため、他のPCと通信するのは厄介です。

Webサーバだけであれば、GitHub Pagesなどでhttps://~を利用することができますが、シグナリングサーバーのWebSocket通信も暗号化する必要がありまり、そちらは自分で証明書を取って対処しなけばなりません。

そこで次回は番外編として、Firebaseを使った暗号化通信をシグナリングに利用した例をご紹介したいと思います。

]]>
https://html5experts.jp/mganeko/20112/feed/ 0
Webブラウザで高速な演算を可能にする低水準言語asm.jsと、WebAssembly詳解ーasm.jsの仕組みとコーディング例 https://html5experts.jp/chikoski/18980/ https://html5experts.jp/chikoski/18980/#comments Mon, 25 Jul 2016 00:06:51 +0000 https://html5experts.jp/?p=18980 連載: 低水準言語asm.jsとWebAssembly詳解 (2)

連載の第1回目は asm.jsの紹介と、asm.jsが導入された背景を概観しました。

Just in Timeコンパイルによって高速にJavaScriptを実行できるようになりましたが、立ち上がりが遅い、やり直しが発生する、コンパイルによって一時的に負荷が向上する、といった問題が残されています。

これを解決するためにプログラムの実行を行うより前にネイティブコードへとコンパイルするAhead of Timeコンパイルを導入したいのですが、JavaScriptは柔軟すぎて効率の良いネイティブコードを出力することが難しい、という問題がありました。

asm.jsはこの問題に一定の解をあたえるものとなります。今回はそのasm.jsがどのようなものなのか、JavaScriptの関数を asm.js化しながら解説していきます。

asm.jsがコンパイルされるまで

前述したとおりasm.jsで記述されたプログラムは、実行以前にコンパイルされます。コンパイルは下図のような過程で行われます。

asm.jsがコンパイルされるまで。意味解析とリンク時のチェックが行われ、失敗すると通常のJSとして実行される。

ソースコードが字句解析、構文解析されてAST(Abstract Syntax Tree:抽象構文木)になるところまでは一緒ですが、その後意味解析が行われます。この意味解析でそれぞれの式や変数、関数の型がチェックされます。

意味解析が終了後、プログラムはコンパイルされネイティブコードが出力されます。このネイティブコードはメモリ上に展開されたあと、プログラム中で利用しているJavaScriptの関数やasm.jsに用意されている標準ライブラリとのリンクが行われます。

意味解析とリンク、この2つに失敗する場合もあります。プログラム中に型エラーが発見された場合は前者の失敗します。 後者はメソッドの呼び出しや、JSにエキスポートできない種類のデータを引数に指定した関数呼び出しを行った場合に失敗します。

このような場合、プログラムはasm.jsとして処理されるのではなく、通常のJSとして処理されます。asm.jsがJSのサブセットであることによって、このようなフォールバックも可能になっています。

asm.jsモジュール

asm.jsで書かれたプログラムとして事前コンパイルされる際の単位は、モジュールです。ファイル単位でコンパイルが行われるわけではないので、1つのJSファイルのうち高速化が必要な部分をasm.jsで書き、それ以外の部分は通常のJSとして書くといったことが可能です。

asm.jsのモジュールは、次のようにCやC++のソースコードと似た構造となっています。

function ModuleName(stdlib, ffi, heap){
"use asm";
 // (1) 外部からインポートするシンボルの宣言
 // (2) 関数宣言
 // (3) 関数表の宣言
 // (4) モジュールのエキスポート
}

1の部分では利用する標準ライブラリや、JSの関数、定数などのシンボルを列挙します。ちょうどCのextern宣言と同じような役割です。

2の部分で、それぞれの処理を関数として定義します。asm.jsではオブジェクトやクラスの定義が許されていません。そのため処理はクラスやオブジェクトとしてではなく、あくまで関数として定義します。

3の部分では同じ型の関数をまとめた表を定義できます。ちょうどCでの関数ポインタの機能を代替するものです。関数を直接呼び出すのではなく、この表を参照する形で呼び出すことで、呼び出す関数の振る舞いを変えられるので、多態性を持った関数を定義したい場合に有用です。

とはいえ、いまいちイメージがつかめないかと思います。そこで足し算を行うモジュールをasm.js化しながら、構成を見てゆきましょう。変更するのは以下のようなモジュールです。

function AddFunctions(){
  function add1(value){
    var result;
    result = value + 1;
    return result;
  }
  return {
    add1: add1
  }
}

このモジュールは次のように利用できます。

const module = AddFunctions();
const one = module.add1(0);    // 1
const two  = module.add1(one); // 2

asm.jsディレクティブ

asm.js化の第一歩は、ディレクティブの追加です。”use strict” ディレクティブをつけると、その関数はstrict modeで解釈されるのと同様に、”use asm”ディレクティブをつけることで、処理系はその関数をasm.jsのモジュール定義として処理します。

function AddFunctions(){
  "use asm";
  function add1(value){
    var result;
    result = value + 1;
    return result;
  }
  return {
    add1: add1,
  }
}

 型アノテーション

次にAOTを行うための型アノテーションを行います。型アノテーションは、TypeScriptなどのように型を直接記述する方法が一般的かと思いますが、JavaScriptとしても解釈できなくてはいけないasm.jsでは異なります。同値となるような式を追加することで、型情報を明示します。

明示的に型アノテーションを行う対象は次の3つです。

  • 関数の引数
  • 変数
  • 関数の返り値

これらの情報を元に、関数の型や式の型が決定されます。

引数に対する型アノテーション

引数に対する型アノテーションは、関数本体の先頭で行います。次の例では、add1の引数valueの型はintであることを示す型アノテーションが加わっています。value = value | 0; が型アノテーションを行っている部分です。

function AddFunctions(){
  "use asm";
  function add1(value){
    value = value | 0;
    var result;
    result = value + 1;
    return result;
  }
  return {
    add1: add1
  }
}

引数で利用できる型はint, doubleの2つです。それぞれの型はは次の表のようにアノテーションします。

型アノテーション
int value = value | 0
doube value = +value
float value = f(value)

尚、外部で定義された関数呼び出しを行った場合は、floatとして解釈されます。

変数宣言

asm.jsでは、関数内で利用する変数に対しても型アノテーションを行います。これは宣言時に初期値として代入するを適切に選ぶ形で行います。整数値を代入すればintに、実数値の場合はdoubleとなります。

尚、1.0のような小数点以下の数字が0のものは実数値として扱われます。変数宣言に型アノテーションをつけると、先ほどまでのモジュールは以下のようになります。

function AddFunctions(){
  "use asm";
  function add1(value){
    value = value | 0;
    var result = 0; // intとして宣言
    result = value + 1;
    return result;
  }
  return {
    add1: add1
  }
}

返り値に対する型アノテーション

返り値に対して型アノテーションを行います。この情報と引数の型情報とを組み合わせて、関数の型が決定されます。

返り値で利用できるのはdouble, signed, float, そしてvoidの4つの型です。それぞれのアノテーション方法は以下の表の通りです。また即値を書く場合は、アノテーションは必要ありません。

アノテーション例 メモ
double return +result;
signed return result 0|
float return f(result) fは関数
void return;

asm.jsの型システム

これまでintやdoubleといった型を利用してきましたが、asm.jsで利用できる型を列挙し、それぞれの継承関係を示すと次の図となります。 矢印の元にある型は先の型を継承しています。

asm.js の型システム http://asmjs.org/spec/latest より引用

継承するということは、何かの制約が厳しくなっていくということです。asm.jsの型システムではnullを許容するか、実行時に割り当てられるレジスタがdoubleかintか、といった点での制約が厳しくなっていきます。

また、JavaScriptで定義されている関数に渡せる型も決まっています。背景色が薄い灰色になっているfixnum / signed / extern / double型のデータのみが許可されています。

型のキャスト

先ほどから変更しているプログラムは、意味解析に失敗します。それは次の部分に原因があります。

result = value + 1;

これはint+fixnumの計算を行い、その結果をint型の変数に代入しようとしています。型エラーの入り込む余地はなさそうに思えます。しかし、この加算の評価値の型はinterishとなっています。そのため、int型へinterish型の値を代入することになり、型エラーが起きるというわけです。

そこでキャストを行い、演算結果の型を明示します。この変更をおこないasm.jsの意味解析に成功するコードは次のようになります。

function AddFunctions(){
  "use asm";
  function add1(value){
    value = value | 0;
    var result = 0; // int として宣言
    result = (value + 1) | 0; // int へキャスト
    return result;
  }
  return {
    add1: add1
  }
}

JavaScriptとの組み込み

以上で、AddFunctionsモジュールをasm.js化することができました。これをJavaScriptのプログラムに組み込みんでいきます。

ここでは 上記で作成したAddFunctionsモジュールをJavaScript側から利用する方法と、AddFunctionsモジュール内で JavaScriptの関数を利用する方法について説明します。

JavaScriptからの利用

asm.jsのモジュールとJavaScriptのモジュールは、JavaScriptからみると区別できません。下記のようにJSのモジュールを呼ぶように利用できます。

const module = AddFunctions();
var one = module.add1(0);
var two = module.add1(one);

function add2(n){
  return module.add1(module.add1(n));
}

JavaScriptの関数をasm.jsから呼ぶには

まず、asm.jsの内部からJavaScriptで定義された関数を呼ぶことはできます。また、Mathオブジェクトの持っているいくつかのメソッドは、標準ライブラリ中の関数として提供されています。

これら関数への参照はモジュールを定義する関数の引数として与えます。例えば、AsmModuleにasm.jsモジュールが定義されている場合、次のように呼び出すことでJavaScriptの関数をasm.js内から呼び出せます。

const ffi = {
  put: n => console.log(n)
};

const module = AsmModule(window, ffi);

asm.jsで定義される関数はオブジェクトの解決ができません。そのため利用する関数はあらかじめ外部からインポートするシンボルとして宣言しておきます。

次の例では、標準ライブラリ中のMath.expとMath.log、そして自作関数であるputを外部からインポートするシンボルとして宣言しています。

function AsmModule(stdlib, ffi, heap){
  "use asm";
  var exp = std.lib.Math.exp;
  var log = std.lib.Math.log;

  var put = ffi.put;

これらの関数は、関数定義内でasm.js内部で定義された関数と同様に呼び出せます。ただ1点注意しなくてはならないのは、引数に渡すデータの型です。標準ライブラリ以外の外部関数に渡せるのはfixnum、signed、extern、doubleのいずれかです。 それ以外の値を渡すとリンクエラーとなり、通常のJSとして実行されます。演算の結果を適切にアノテーションすることで、リンクエラーを避けられます。

var value = 1;
put(value + 1); // リンクエラー
put((value + 1) | 0); // OK

ヒープの利用

asm.jsで定義される関数は、数値演算しかできません。また、オブジェクトの解決もできません。つまり、次のような関数は定義できないことになります。

function caesar(string, key){
  var result = "";
  for(let i = 0; i < string.length; i++){
    result += String.fromCharCode(a.charCodeAt(0)+key);
  }
  return result;
}

ところでC言語では文字列を数値の配列として扱います。この考えを応用すれば、asm.jsでも文字列を数値演算の範囲で扱えるようになります。

上記の関数をasm.jsに書き直すと以下のようになります。

function Caesar(stdlib, ffi, heap){
  "use asm";
  var HEAP = new stdlib.Int8Array(heap);

  function encrypt(key){
    key = key | 0;
    var i = 0;
    for(;(HEAP[i << 0 >> 0] | 0) != 0; i = i + 1 | 0){
      buffer[i << 0 >> 0] = ((buffer[i << 0 >> 0] | 0) + key) | 0;
    }
    return;
  }

文字列はHEAPというArrayBufferに格納されています。このArrayBufferはモジュールの定義時に引数として与えられます。 ArrayBufferのビューは標準ライブラリとして提供されているため、上記のようにモジュール内のHEAPを大域変数として宣言する際にビューもあわせて定義します。

HEAPの添字は、ビューの各要素の大きさに合わせてシフトする必要があります。シフトするビット数は、2を底として要素のバイトサイズのlogをとると求まります。

上記の例で利用しているInt8Arrayの場合、各要素の大きさは1バイトのため、0ビットシフトしています。ビューとシフトするビット数の対応は次の表を参照してください。

ビュー 要素のサイズ(バイト) シフトするビット数 ロード時の型 保存時の型
Uint8Array 1 0 intish intish
Int8Array 1 0 intish intish
Uint16Array 2 1 intish intish
Int16Array 2 1 intish intish
Uint32Array 4 2 intish intish
Int32Array 4 2 intish intish
Float32Array 4 2 float? floatish, double?
Float64Array 8 3 double? float?, double?

ArrayBufferを与えてモジュールの作成と関数の呼び出しを行うと、次のようなコードとなります。

気をつけなければならいのは、TypedArrayの大きさです。212 以上、224バイト未満の大きさになるようにするか、224バイトの整数倍の大きさになるようにしてください。そうしなければ、リンクに失敗してします。

これが原因でリンクに失敗した場合は、コンソールに適切なサイズが表示されます。それを参考に大きさを際設定すればようでしょう。

const heap = new Int8Array(0x10000)
const caesar = Caesar(window, {}, heap);
heap[0] = 72; heap[1] = 65; heap[2] = 76; // HALと設定
caesar.encrypt(1); // HALが1文字ずつシフトされる

まとめ

以上のように、JavaScriptと比べてasm.jsは随分と書きづらく、できることも限られています。ArrayBufferを駆使すればベクトルの計算も可能ですが、オブジェクトと平坦なArrayBufferとの相互変換を自分で実装しなくてはならず、なかなか骨が折れる作業であることは否めません。

その代わり得られる効果は絶大です。JITによって処理が重たくなることもなく、高速な実行が可能となります。またコンパイルされた結果はキャッシュされるため、2回目以降は高速に起動できるようになります。

とはいえ、手で書くのは骨が折れます。

「人間のやることではない」

「高級言語で実装したい」

そう思う方も多いでしょう。そのために用意されているツールがEmscriptenです。次回はEmscriptenを利用したC言語やC++で実装されたコードのasm.jsへの変換について解説します。

]]>
https://html5experts.jp/chikoski/18980/feed/ 0
シグナリングサーバーを動かそう ーWebRTC入門2016 https://html5experts.jp/mganeko/20013/ https://html5experts.jp/mganeko/20013/#comments Thu, 14 Jul 2016 00:45:36 +0000 https://html5experts.jp/?p=20013 連載: WebRTC入門2016 (3)

こんにちは! 2014年に連載した「WebRTCを使ってみよう!」シリーズのアップデート記事も3回目となりました。今回は、前回の「手動」で行ったP2P通信の準備を、自動で行えるようにしてみましょう。

シグナリングサーバーを立てよう

前回は手動でコピー&ペーストを行い、WebRTCのP2P通信を始めるために次の情報を交換しました。

  • SDP
  • ICE candidate

今回はこれを仲介するサーバー(シグナリングサーバー)を動かしてみましょう。方法として次の2つをご用意しました。

  • Node.jsを使ったシグナリングサーバー
  • Chromeアプリ

Node.jsを準備しよう

まず、WebSocketを使ってシグナリングを行う方法をご紹介します。WebSocketの扱いやすさから、ここではNode.jsを使います。(もちろん他の言語を使っても同様にシグナリングサーバーを作ることができます)こちらの公式サイトから、プラットフォームに対応したNode.jsを入手してインストールしてください。今回私は 4.4.7 LTSを使いました。

Node.jsのインストールが完了したら、次はWebSocketサーバー用のモジュールをインストールします。コマンドプロンプト/ターミナルから、 次のコマンドを実行してください。 ※必要に応じて、sudoなどをご利用ください。

npm install ws

以前の連載ではsocket.ioを使いましたが、今回はよりプリミティブなwsを使っています。

シグナリングサーバーを動かそう

次のコードを好きなファイル名で保存してください。(例えば signaling.js)

"use strict";

let WebSocketServer = require('ws').Server;
let port = 3001;
let wsServer = new WebSocketServer({ port: port });
console.log('websocket server start. port=' + port);

wsServer.on('connection', function(ws) {
  console.log('-- websocket connected --');
  ws.on('message', function(message) {
    wsServer.clients.forEach(function each(client) {
      if (isSame(ws, client)) {
        console.log('- skip sender -');
      }
      else {
        client.send(message);
      }
    });
  });
});

function isSame(ws1, ws2) {
  // -- compare object --
  return (ws1 === ws2);     
}

ポート番号は必要に応じて変更してください。起動するにはコマンドプロンプト/ターミナルから、 次のコマンドを実行します。

node signaling.js

シグナリングサーバーの動作はシンプルで、クライアントからメッセージを受け取ったら他のクライアントに送信するだけです。

Chromeアプリを使う場合は

場合によってはNode.jsをインストールして動かすのは、ハードルが高くて難しいケースもあるかもしれません。そんな人のために、Chromeアプリで「simple message server」というものを作ってみました。 simple_message_server_store
Chromeを利用したアプリとしてインストールし、アプリタブから起動して利用します。デスクトップ用のChromeが動く環境(Windows, MaxOS X, Linux, ChromeOS)で動くはずです。

起動すると、 ws://localhost:3001/ でクライアントからの接続を待ち受けます。※実装があまいので時々不安定になります。その場合は[restart]ボタンを押してリセットし、ブラウザもリロードして接続しなおしてください。

シグナリング処理を変更しよう

それでは前回の手動シグナリングのコードを、少しずつ変更していきましょう。まずWebSocketで用意したシグナリングサーバーに接続します。JavaScriptに次の処理を追加してください。(URLは使っているポートに合わせて修正してください)

let wsUrl = 'ws://localhost:3001/';
  let ws = new WebSocket(wsUrl);
  ws.onopen = function(evt) {
    console.log('ws open()');
  };
  ws.onerror = function(err) {
    console.error('ws onerror() ERR:', err);
  };

次に、WebSocketでメッセージを受け取った場合の処理を追加します。

ws.onmessage = function(evt) {
    console.log('ws onmessage() data:', evt.data);
    let message = JSON.parse(evt.data);
    if (message.type === 'offer') {
      // -- got offer ---
      console.log('Received offer ...');
      textToReceiveSdp.value = message.sdp;
      let offer = new RTCSessionDescription(message);
      setOffer(offer);
    }
    else if (message.type === 'answer') {
      // --- got answer ---
      console.log('Received answer ...');
      textToReceiveSdp.value = message.sdp;
      let answer = new RTCSessionDescription(message);
      setAnswer(answer);
    }
  };

JSONテキストからオブジェクトを復元し、typeに応じて前回用意したsetOffer()/setAnswer()を呼び出し、RTCPeerConnectionに渡しています。

SDPの送信

Offer/AnswerのSDPの送信も、WebSocket経由で行います。前回要したsendSdp()を次のように変更します。

function sendSdp(sessionDescription) {
    console.log('---sending sdp ---');

    textForSendSdp.value = sessionDescription.sdp;
    /*--- テキストエリアをハイライトするのを止める
    textForSendSdp.focus();
    textForSendSdp.select();
    ----*/

    // --- シグナリングサーバーに送る ---
    let message = JSON.stringify(sessionDescription);
    console.log('sending SDP=' + message);
    ws.send(message);
  }

SDPをJSONテキストに変換してWebSocketでシグナリングサーバーに送信しています。

実際に動かしてみよう

シグナリングサーバーを起動して、ChromeかFirefoxのウィンドウを2つ開いて修正したHTMLを読み込んでください。ChromeとFirefoxの間で通信することもできます。

Webサーバーを立てるのが難しい場合は、GitHub Pages にもサンプルを公開しているので、そちらで試すこともできます。その場合でもシグナリングサーバーは自分で用意する必要があるのでご注意ください。

(1) カメラの取得

両方のウィンドウで[Start Video]ボタンをクリックします。カメラのアクセスを許可すると、それぞれリアルタイムの映像が表示されます。
ws_signaling_startvideo

(2) 通信開始

どちらかのウィンドウで[Connect]ボタンを押します。(3)SDP(ICE candidateを含む)が自動で交換され、(4)ビデオ通信が始まります。
ws_signaling_connect

手動シグナリングに比べて操作がずっと簡単になりました。これなら実際に利用できそうですね。

Trickle ICE を使ってみよう

コピー&ペーストを手動で行う必要がなくなったので、ICE candidateを発生するたびに交換するTrickle ICE を使ってみましょう。流れはこのような形になります。
hand2016_trickle
すべてのICE candidateが出そろう前にP2P通信が確立する(ことがある)メリットがあります。(※2014年の記事では「すべてのICE candidateの交換が終わるとP2P通信が始まる」と書いていましたが、これは誤りです)

SDPをすぐに送信する

Offer SDP/Answer SDPを生成したら、すぐに相手に送るように変更します。

function makeOffer() {
    peerConnection = prepareNewConnection();
    peerConnection.createOffer()
    .then(function (sessionDescription) {
      console.log('createOffer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICE の場合は、初期SDPを相手に送る -- 
      sendSdp(peerConnection.localDescription); // <--- ここを加える

      // -- Vanilla ICE の場合には、まだSDPは送らない --
    }).catch(function(err) {
      console.error(err);
    });
  }

  function makeAnswer() {
    console.log('sending Answer. Creating remote session description...' );
    if (! peerConnection) {
      console.error('peerConnection NOT exist!');
      return;
    }
    
    peerConnection.createAnswer()
    .then(function (sessionDescription) {
      console.log('createAnswer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICE の場合は、初期SDPを相手に送る -- 
      sendSdp(peerConnection.localDescription); // <--- ここを加える

      // -- Vanilla ICE の場合には、まだSDPは送らない --
    }).catch(function(err) {
      console.error(err);
    });
  }

ICE candidateも、すぐに交換する

ICE candidateを収集した際も、すぐに送るように変更します。

function prepareNewConnection() {
    // ... 省略 ...

    // --- on get local ICE candidate
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);

        // Trickle ICE の場合は、ICE candidateを相手に送る
        sendIceCandidate(evt.candidate); // <--- ここを追加する

        // Vanilla ICE の場合には、何もしない
      } else {
        console.log('empty ice event');

        // Trickle ICE の場合は、何もしない
        
        // Vanilla ICE の場合には、ICE candidateを含んだSDPを相手に送る
        //sendSdp(peer.localDescription); // <-- ここをコメントアウトする
      }
    };

    // ... 省略 ....
  }

  function sendIceCandidate(candidate) {
    console.log('---sending ICE candidate ---');
    let obj = { type: 'candidate', ice: candidate };
    let message = JSON.stringify(obj);
    console.log('sending candidate=' + message);
    ws.send(message);
  }

合わせてICE candidateをWebSocket経由で受け取った場合の処理も追加しましょう。相手からICE candidateを受け取ったら、その度にRTCPeerConnection.addIceCandidate()で覚えさせます。

ws.onmessage = function(evt) {
    console.log('ws onmessage() data:', evt.data);
    let message = JSON.parse(evt.data);
    if (message.type === 'offer') {
      // -- got offer ---
      console.log('Received offer ...');
      textToReceiveSdp.value = message.sdp;
      let offer = new RTCSessionDescription(message);
      setOffer(offer);
    }
    else if (message.type === 'answer') {
      // --- got answer ---
      console.log('Received answer ...');
      textToReceiveSdp.value = message.sdp;
      let answer = new RTCSessionDescription(message);
      setAnswer(answer);
    }
    else if (message.type === 'candidate') { // <--- ここから追加
      // --- got ICE candidate ---
      console.log('Received ICE candidate ...');
      let candidate = new RTCIceCandidate(message.ice);
      console.log(candidate);
      addIceCandidate(candidate);
    }
  };

  function addIceCandidate(candidate) {
    if (peerConnection) {
      peerConnection.addIceCandidate(candidate);
    }
    else {
      console.error('PeerConnection not exist!');
      return;
    }
  }

さあ、これで修正は完了です。

Trickle ICEを実行しよう

手順はVanilla ICEの場合と同じです。シグナリングサーバーを起動して、ChromeかFirefoxのウィンドウを2つ開いて修正したHTMLを読み込んでください。あとは同様に[Start Video]→[Connect]です。

見た目も特に変わりはありません。もしかしたら人によっては早く繋がるのを実感できるかもしれません。

GitHub Pages/GitHubも用意しています。

2台のPC間の通信

ここまできたら、せっかくなので2台の別々のPCで通信してみたくなります。同じネットワークに属するPC同士ならば通信できるはずです。例として次のような状況を考えてみましょう。

  • IPアドレスが 192.168.0.2 と、 192.168.0.3 の2台のPCがある
  • 前者(192.168.0.2)のポート:8080でWebサーバー、ポート:3001でNode.jsのシグナリングサーバーが動いている
    2pc_firefox

Firefoxの場合は、接続するURLを変更すれば問題なく動きます。やっかいなのはChromeの場合です。

  • Chromeでは、カメラやマイクにアクセスするためのgetUserMedia()が、原則としてhttp://~では許可されていない
  • http://localhost/~ は例外的な扱いで許可されている
    2pc_chrome

きちんと対処すると、次のような対策が必要です。ちょっと試すにはハードルが高いですよね。

  • 証明書を取得して https://~ でアクセスするように、Webサーバーに設定
  • 合わせて、シグナリングサーバーも wss://~ の暗号化通信を使うように設定

そこで実験的に無理やり動かすには、次のような方法があります。Webサーバーとシグナリングサーバーは同一である必要はなく、また異なるWebサーバーでも構わないことを利用しています。
2pc_chrome_force

お勧めはしませんが、どうしてもやりたい場合の参考としてどうぞ。

次回は

今回はNode.jsとWebSocketを使ったシグナリングを実現しました。残念ながら今回の仕組みでは、1対1の通信しか行うことができません。次回はこれを拡張し、複数人で同時に通信できるようにしたいと思います。

オマケ:WebRTCの仕様の差分のおさらい

オマケとして、今回のWebRTC再入門2016シリーズで取り上げているWebRTC関連仕様の変更箇所について、おさらいしおきましょう。(2016年6月現在)

getUserMeida

  • navigator.mediaDevices.getUserMedia() が新しく用意された
    • 旧APIの navigator.getUserMedia()は Firefoxでは非推奨
  • ベンダープレフィックスが取れた
  • コールバックではなくPromiseベースになった
  • Firefox, Edge で利用可能。Chromeではフラグ指定が必要

ベンダープレフィックスの除去

  • Firefoxでは、主要なオブジェクトのベンダープレフィックスが取れた。mozプレフィックス付は非推奨に
    • 新:RTCPeerConnection, RTCSessionDescription, RTCIceCandidate
    • 旧:mozRTCPeerConnection, mozRTCSessionDescription, mozRTCIceCandidate (非推奨)
  • ただしChromeでは、一部ベンダープレフィックス付のまま
    • プレフィックス有り: webkitRTCPeerConnection
    • プレフィックス無し: RTCSessionDescription, RTCIceCandidate

RTCPeerConnection

  • 主要なメソッドがPromiseベースになった
    • createOffer(), createAnswer()
    • setLocalDescription(), setRemoteDescription()
  • メディアストリーム処理の新しいイベントハンドラontrack()が追加、onaddstream()は非推奨
    • Firefoxではサポート済、Chromeでは未サポート

仕様は常に更新されていますし、ブラウザの実装状況も異なります。最新の情報もご確認ください。

]]>
https://html5experts.jp/mganeko/20013/feed/ 0
HTTPSにまつわる怪しい伝説を検証する──Google I/O 2016 セッションレポート https://html5experts.jp/takoratta/20061/ https://html5experts.jp/takoratta/20061/#comments Wed, 13 Jul 2016 02:00:08 +0000 https://html5experts.jp/?p=20061 今年はGoogle I/Oに初めて社員ではない立場で参加しました。全体の感想は Google I/O 2016まとめ(Web的視点) で公開していますが、今回はその中で、気に入ったセッションの1つである”Mythbusting HTTPS: Squashing security’s urban legends”について書いてみたいと思います。

セッションは大変良くまとまっていますので、YouTubeにあがっている動画を見れる人はそちらを見てもらえればいいのですが、時間がないという人のために、その内容をまとめました。基本的には文字起こしに近いものです。

重要だとわかっているけど、なかなか導入に踏み切れない人も多いHTTPS。これについて、最新の状況が理解できるコンテンツとしてお役に立てるならば嬉しいです。

※この記事は、Qiitaに投稿された記事を、Qiitaの許可を得て転載したものです。

TL;DR

  • HTTPSはPWAppなどWebにとって必須。
  • しかし、パフォーマンス悪化するかも!? でもお高いんでしょ? それに見合うだけのメリットあるの?などなどの疑問(=都市伝説的なものも含む)があるので、それを1つ1つ検証。
  • 結論: 確かに懸念すべきものもあるが、やはり必須。正しく、早急に導入しよう。

Mythbusting HTTPS: Squashing security’s urban legends

“Mythbusting HTTPS: Squashing security’s urban legends” はGoogle I/O 2016の初日の夕方に行われたセッションです。ChromeのセキュリティチームでSSL/TSLを担当するエンジニアであるエミリー(Emily Stark)自らが登壇しました。

Mythbustingとは

Mythとは英語で「神話」を意味します。Bustにはいくつか意味がありますが、ここではゴーストバスターズなどと同じように、「退治する」、「撃退する」の意味になります。実は、オーストラリアで制作され、米国でも放映されているMythbustersという人気テレビ番組があります。都市伝説のような伝説を検証する科学エンターテイメント番組なのですが、タイトルのMythbustingはもしかしたらそこからとったのかもしれません。

Mythbustersは日本でもケーブルテレビなどで放映されておりますが、そのタイトルは「怪しい伝説」となっています。この投稿のタイトルもそれに習いました。

セッション動画

セッション動画はここから。英語が苦手な方も、Google Developers Japan: 技術と英語を同時に、しかも無料で勉強できる画期的な方法 にあるように字幕付きならば、技術的な内容ですので、十分内容を掴めるはずです。

HTTPSのおさらい

セッションでは、スピーカーであるエミリーが2010年の世界から見ると、2016年の今日のWebは大きく進化したというところから話し始めます。ホーム画面への追加(Add to Home screen)やプッシュ通知、デバイスオリエンテーション、ジオロケーションなどプログレッシブWebアプリケーションをはじめとするWebをよりアプリに近づける技術が使われるようになっています。

一方で、そのようなアプリケーション的なWebが普及する中、未だに課金や商取引、個人情報をやりとりするようなデータを扱っていながら、第三者から盗み見されるようなネットワークで運用されているところもあったり、ISPやWiFiプロバイダーのような第三者がWebサイト運営者の意図しないようにコンテンツを書きかえることさえ起きています。

データ盗聴の危険性

意図しないコンテンツの挿入

エミリーはだからこそHTTPSが重要だと強調します。

現在のブラウザはHTTPSでアクセスしているサイトには、それがHTTPSであることをインディケーターで示します。ですが、すべてのWebサイトがHTTPSをサポートし、この緑のインディケーターを必要としない世界こそ、望むべきものです。

HTTPSには3つの役割があります。

HTTPSの役割

  • 認証(Identity): https://google.com にアクセスすると、ブラウザはgoogle.comから証明書を受信し、それにより今アクセスしているgoogle.comが真正のgoogle.comであることを確認します。
  • 守秘性(Confidentiality): 一度、真正のgoogle.comにアクセスしていることが確認されると、その後の通信はgoogle.comとブラウザ以外には盗聴されない形で行われます。
  • 完全性(Integrity): google.comとブラウザの間でのデータは第三者により改ざんされないことが保証されます。

このようなセキュリティ上の利点があるものの、HTTPSを導入しようとしたときに、コスト、パフォーマンス、そして保守の面で課題があるのではないかと心配されるかもしれません。しかし、それは過去のものです。そのようにエミリーは言います。

HTTPSに関しての誤解

HTTPSにまつわる話

これらの誤解は結局のところ、5つほどの話に整理できます。それらのうちのいくつかは過去には正しかったものの、現在では都市伝説になっているものもあります。エミリーは5つの話を現在でも事実の話と怪しい伝説とに分類していきます。

5

その5つの話は次の通りです。

  1. 私のサイトはHTTPSを必要とするほど重要ではない
  2. HTTPSを導入すると遅くなる
  3. HTTPSに関しての攻撃がたくさんあるみたい
  4. HTTPSにはお金がたくさんかかる
  5. 私のサイトはHTTPSに移行できるけど、使っている(依存している)サードパーティ(のサービス)はどうしたらいいの?

それでは、エミリーが1つ1つをどのように解説していったかをお伝えしましょう。

「私のサイトはHTTPSを必要とするほど重要ではない」

エミリーはアリスの話を始めます。

アリスというWebデベロッパーがいます。彼女の旅行ガイドのWebサイトはログインフォームもなければ、クレジットカード番号の入力フォームもない。そこでアリスは自分のサイトにはHTTPSは必要ないと考えます。ですが、ある日、彼女の友人からアリスの旅行ガイドサイトが遅いと言われて、調べてみると、友人が見ている彼女のサイトには彼女が入れたのではない様々な広告が挿入されていたことに気づくのです。それらにはマルウェアが埋め込まれていて、アリスのサイトを信じた友人はサイト内の広告もクリックし、マルウェアに感染してしまっていたのです。

アリスはまた新しい機能をジオロケーションAPIを用いて実装しようとします。ユーザーがどこにいるかを把握し、適切なトラベル情報を提供しようと試みたのです。しかし、彼女はChromeでは動作しないことに気づきます。

ジオロケーションAPI

現在、Chromeを始めとする多くのモダンブラウザはジオロケーションAPIをはじめとするパワフルな新しいAPIの利用はセキュアはHTTPS上でしか動作しないようにし始めています。

HTTPSを必要とする機能

このように、HTTPSはプライバシーやセキュリティ上の重要なデータのやりとりが発生するようなサイトだけでなく、ユーザー体験を向上させるすべてのサイトに必要なものとなっています。

つまり、この話はです。

「HTTPSを導入すると遅くなる」

次はボブの話。ボブはEコマースサイトを持っています。彼はサイトを分析し、コンバージョン最適化のためにはレイテンシーを下げることが必要と判断しましたが、HTTPSとパフォーマンスについてあまりよくない話を聞きました。

HTTPSに移行したyell.comはパフォーマンスの差(HTTPとHTTPS)は相対的に大きかったと言っています。しかしながら、2010年にGmailをHTTPSに移行したGoogleは無視できるほどの差しかなかったとしています。

ボブはこれらの話を知り、さらに自分自身でも調査をしました。

エミリーはボブや一般のWebデベロッパーのためにHTTPSにするとネットワーク層で何が起きるのかを説明し、どのように遅延を避けるかを解説します。

HTTPS移行で起きること

一般のユーザーはブックマークするなどした以前のHTTPのURLでサイトにアクセスしてきますので、まずはHTTPからHTTPSへのリダイレクトが起きます。そしてその次にTLSハンドシェイクで証明書のやりとりと双方がTLSを使えることの確認が行われます。

HSTS

まず最初のHTTPからHTTPSへのリダイレクトに関しては、HTTP Strict Transport Security(HSTS)の導入を勧めます。HTTPヘッダーにHSTSの記述をすることで、次回からのアクセス時にブラウザはHTTPでのURLを自動的に内部でHTTPSに置き換えます。これにより2度目以降はこのリダイレクトは発生しなくなります。

注意事項としては、これは一度セットされると、HTTPヘッダーが失効するまで有効ですので、HTTPSのテスト時や完全にHTTPSでのアクセスをサポートできるまでは行ってはいけません。

TLS False Start

次のTLSハンドシェイクにおける最適化は2ウェイラウンドトリップをいかに高速化するかに関わるのですが、ここではTLS False Startの利用を勧めます。

TLS False Start

TLS False Startは2ウェイラウンドトリップを待つことなく、クライアントであるブラウザがHTTPSでの通信をリクエストするものです。

TLS Session Resumption

もう1つの最適化はTLS Session Resumptionと呼ばれるもので、一度TLS通信を行った場合、以前に使ったセッションIDを用いることで、同じTLSハンドシェイクは必要ないと省略するものです。

TLS Session Resumption

HTTP/2

ここでエミリーはHTTP/2のいくつかの特徴を話します。

HTTP/2はリクエストとレスポンスを並列で行うパイプライン化により、1つのリクエストのレスポンスを待たずに次のリクエストを送れます。また、サーバープッシュにより、ブラウザがHTMLをパースし、追加のリソースであるスタイルシート(CSS)やJavaScript、イメージなどをリクエストするのを待たずに、サーバー側から必要となるリソースをプッシュすることが可能です。

HTTP/2には他にもさまざまな利点があります。

ここで大事なのは、このHTTP/2はHTTPSでのみ利用可能なことです。

エミリーはHTTP/2がHTTPSでのみ利用可能になっているのには、2つの理由があると言います。1つがHTTPSを普及させるためのインセンティブとして、つまりHTTP/2を使いたいならば、HTTPSを使わなければいけないとすることにより普及を目論んでいるということです。もう1つがプロキシサーバーなどの中継機器によりHTTP/2の通信が阻害されることを避けるためです。

ここで、さきのボブの話に戻ります。ボブが知ったyell.comのHTTPS移行の話ですが、実は彼らが遭遇したパフォーマンス上の課題は古いロードバランサーによるものであったそうです。彼らは将来的にはHTTP/2にアップグレードすることで、TLSネゴシエーションを改良し、RTTを減らすことで、HTTPSへの移行をネガティブなインパクトではなく、ポジティブなインパクトに変えたいと言っています。

実際に、別の事例としてweather.comがあります。彼らはHTTPSに移行した際にネガティブなインパクトがあったのですが、最終的にHTTP/2にアップグレードすることで、それらはほとんど解消できたそうです。

ボブはこれらの調査結果を踏まえて、解説されたような設定を行うとともに、HTTP/2にアップグレードすることで、パフォーマンスの心配なく、HTTPSに移行することができました。

結論として、この話、「HTTPSを導入すると遅くなる」は(ほとんど)嘘とエミリーは言います。「ほとんど」となっているのは、HTTP/2へのアップグレードなどを含めて総合的にはという意味でしょう。

参考情報

HTTPSに移行したyell.comの話はHTTPS is Hard – The Yell Blogに詳しく書かれています。これは大変読み応えのある記事なので、英語ですがお勧めです。

また、HSTSについては以前書いたHSTS (HTTP Strict Transport Security) の導入 – Qiitaに簡潔にまとめてあります。

HSTSを含むTLSの設定などは、IPA(情報処理推進機構)のSSL/TLS暗号設定ガイドライン~安全なウェブサイトのために(暗号設定対策編)~がお勧めです。

「HTTPSに関しての攻撃がたくさんあるみたい」

今度はイブの話。技術的な知識もあるイブはちょっとお金に困っていました。そこでHTTPSの脆弱性の話に目をつけ、あるHTTPSの脆弱性を持ったまま放置されているサイトを探します。クレジットカードやログイン情報を持っているサイトなどで脆弱性が放置されているサイトにアクセスします。

これは実際に起こりうる話です。

TLS脆弱性

HTTPSのセキュリティの研究は最近とても注目されており、単なる攻撃の可能性を理論で示すだけではなく、より実践的な形で攻撃を示したり、インターネット上で実際に脆弱なサイトを見つけるツールやスクリプトを公開したりすることも多くなっています。

実際に図に示したように、多くの脆弱性と攻撃が見つかっています。

エミリーはこう考えたらどうかと言います。

「HTTPSはライフジャケットのようなもの。いろいろと問題はあるけれど、HTTPはライフジャケット無しのようなもの」

HTTPSを使わないのではなく、ツールなどを用いて、サイトで用いる技術を常に最新の状態に保つのが良いでしょう。例えば、SSL Labsスキャニングツールを提供していて、サイトのURLを入力することで、サイトのレイティングや必要となる設定などを示してくれます。

https://qiita.comのスキャン結果

類似のツールとして、MozillaはSSL Configuration Generatorを提供しています。利用するサーバーやバージョンなどを入力することで、設定が生成されます。

Mozilla SSL Configuration Generator

さて、この話の結論は事実(ただし、心配しすぎることは無い)です。「心配しすぎることはない」のは、ライフジャケットなしのHTTPよりも、多少の問題があってもライフジャケットとなるHTTPSは使ったほうが良いし、問題もツールなどを用いて常に最新ソフトウェアを使い、構成を正しくすることで解決できるからです。

「HTTPSにはお金がたくさんかかる」

次の主人公はチャーリー。

チャーリーは2年前にスタートアップ向きの素晴らしいアイデアを思いつきました。彼はサイトを作り、そこで人々がチャットを行えるようにし、無事ベンチャーキャピタルからも資金を調達できました。それから2年経ち、資金調達で得た資金も枯渇しつつありこともあって、彼はコストには厳しくなってきています。HTTPSにはお金がかかるという話が気になっています。

14

お金はHTTPSのいろいろなところに関係しています。

パフォーマンスもお金に関係しますし、最後に紹介する広告もお金の話です。ここでは、特にHTTPSの財務上の課題について話します。HTTPSを支える暗号技術における認証は証明書が必要です。これは従来は認証局から有償で購入する必要がありました。身分証明や法人登記書類などを提示して、認証局より証明書を購入し、それをブラウザに対しての自らの認証として使います。

さて、この証明書はいくらかかるのでしょう? 確かに以前はお金がかかりましたが、今ではいくつかの安価な解決策があります。例えば、プロジェクトSSLMate(https://sslmate.com )というものが数年前から始まっていますが、ここでは1つのドメイン年額$15.95です。複数証明書や追加オプションなどを使うとしても、財務にさほど大きな負担を与えません。新しいプロジェクトのLet’s Encrypt(https://letsencrypt.org/ )は無料で証明書を発行します。

このSSLMateとLet’s Encryptは無料もしくは安価での証明書の取得を実現するだけでなく、自動化のためのコマンドラインツールも提供しているので、期限切れを防ぐことなどの管理作業を行うことができます。

このように、証明書はあまりお金がかからないものとなりました。

もう1つのお金にまつわる心配は、検索ランクへの影響です。HTTPとHTTPSという2つのバージョンのサイトがあったならば、検索エンジンが混乱してしまい、検索ランクが下がってしまうのではないでしょうか?

Googleはサイトの移動に関していくつかのガイドラインを示しています。1つはサイトを移動した場合、301リダイレクトを返すようにと勧めています。また、検索エンジンのクローラーがアクセスしてきた時のために、canonicalリンク要素を提供する必要があります。

15

他のガイドラインとしては、次のようなものがあります。

https://support.google.com/webmasters/answer/6033049 https://support.google.com/webmasters/answer/6073543 https://plus.google.com/+JohnMueller/posts/PY1xCWbeDVC

これらのガイドラインに従うことで、検索ランクへの影響は最小限に抑えることができ、その影響もしばらくしたら回復します。逆に、GoogleはHTTPSのサイトにランキングブーストを行っています。現在は小さいブーストだが、将来的には変更もあり得ることをエミリーは示唆しています。

証明書のコストも無料か低価格で抑えられ、検索結果への影響も無視できるレベルなので、この話の結論はとなります。

「私のサイトはHTTPSに移行できるけど、使っている(依存している)サードパーティ(のサービス)はどうしたらいいの?」

最後の話の登場人物はフランシスコ。彼によるサードパーティコンテンツにまつわる話です。 皆さんのサイトも多くのサードパーティに依存していることでしょう。

フランシスコは大きなニュースサイトを運営しています。このニュースサイトには多くのレガシーなコンテンツも掲載されています。フランシスコはサイトをHTTPS化すると、すべてのサードパーティコンテンツも同様にHTTPS対応していなければならないと聞いていたので、サイトのHTTPS化はかなり大変ではないかと心配しています。

これは事実で、サードパーティコンテンツもHTTPSに対応していなければなりません。

まず一番最初に心配したのが、サイトの収入源でもある広告です。広告がHTTPSに対応していれば、サイトの移行もすぐに行なえます。GoogleのAdSenseは現在では常にHTTPS上で提供されるようになっています。そのため、フランシスコが自分のサイトをHTTPSに移行する前であっても、実はAdSenseはHTTPSですでに提供されています。つまり、AdSenseについては心配の必要はありません。ただし、HTTPS通信がブロックされているいくつかの国は例外です。

これはGoogleだけの動きではありません。業界全体のトレンドです。2015年、IAB(Interactive Advertising Bureau)はブログ記事の中で約80%の広告ネットワークがすでにHTTPSに対応していると明かしています。

Adopting Encryption: The Need for HTTPS

このように最近では広告がHTTPSに対応していないから、サイトのHTTPS化ができないというのはほとんどなくなっています。もし、広告ネットワークがHTTPSに対応していないようだったら、是非確認してみてください。近い将来に対応予定であることがほとんどでしょうし、もしそうでなかったならば何故対応しないか、是非対応してほしいと言ってみましょう。

次に心配なサードパーティコンテンツはHTTPリファラーヘッダーに依存しているものです。アクセス解析のためなどのいくつかの理由によりパートナーサイトであるサードパーティはHTTPリファラーヘッダーを見て、トラフィックがフランシスコのサイトから来たものであることを確認します。問題は、HTTPSでホストされているサイト上のHTTPでホストされているサイトへのリンクをユーザーがクリックした場合、ブラウザはプライバシーの理由からリファラーヘッダーを取り去ってしまうのです。フランシスコにとって、これは困ります。

ここで解決のために必要となるのが、Referrer Policyです。a要素などに付けられるこのReffer Policyとして、”origin-when-cross-origin”を指定することで、リファラーヘッダーがHTTPサイトに対して送られることになります。他にも複数のポリシーがあるので、適切なものを選び、必要な状態でリファラーを送ることができます。

&lt;a href="http://external-partner.com/..." referrerpolicy="origin-when-cross-origin"&gt;Click here!&lt;/a&gt;

最後のフランシスコの心配は少し一般的なものです。Mixed Contentsと言われる、HTTPSサイトでホストされているページの中にHTTPサイトでホストされるセキュアではないコンテンツが含まれるケースです。このようなコンテンツがロードされることで、HTTPSにより高められるセキュリティが妥協したものになってしまうため、厳しい措置がとられています。スクリプトやiFrameのようなものはブラウザによりブロックされます。

また、フランシスコのサイトでは多くの古い記事にロードされている写真などがあったのですが、これらはHTTPでホストされています。このようなイメージに対してはブラウザはブロックはしませんので、イメージも見ることはできますが、ブラウザのアドレスバーのHTTPSアクセスを示す緑色は消えてしまいます。

CSP(Content Security Policy)を用いてこの問題は対処することができます。HTTPレスポンスヘッダーに次のように指定します。

Content-Security-Policy-Report-Only: default-src https:
  'unsafe-inline' 'unsafe-eval'; report-uri
  https://example.com/reportEndpoint

default-src https:という指定から、ブラウザはすべてのコンテンツはHTTPSでロードしようとします。ただし、'unsafe-inline' 'unsafe-eval'という指定から、動的に生成されたものやインラインスクリプトは例外とします。ブラウザがコンテンツをロード中に、もしこのポリシーに従わなかったものを見つけたら、https://example.com/reportEndpointにレポートするように指示されています。

このポリシーの例は、”Report-Only”なので、ユーザーから見た時の挙動は変わりません。セキュアでないコンテンツのロードがブロックされることもありません。ただ、もしそのようなコンテンツがロードされたことがあったならば、フランシスコはレポートを通じて知ることができます。

このレポーティングのためのインフラを用意するのも面倒であった場合、report-uri.ioというサービスを使うことも可能です。エンドポイントを用意してくれるだけでなく、解析やビジュアリゼーションまでも行ってくれます。

report-uri.io

ここで説明したことは、Chrome DevToolsのセキュリティパネルで見ることができるようになっています。

DevTools Security Panel

この話の結論は真実です。ですが、ここ数年で状況は改善されつつあり、業界全体として対応に取り組んでいるものです。

まとめ

確かに、10年や15年前はHTTPSは遅く、導入にはコストがかかり、セットアップは手間のかかるものでしたが、現在では多くの障壁は取り払われています。

このセッションでエミリーが解説したように、HTTPS関連のツールやサービスなどが充実し、導入は昔の比ではないほど敷居が下がっています。インターネットのセキュリティの強化は誰かひとりや一社の取り組みで一日にして成るものではありません。HTTPSの導入を進めることで、安全に快適なインターネット空間が広がるよう、皆で努力していきましょう。

補足

各お話に出てくる人物の名前にはそれぞれ由来があります。

  • アリスとボブは暗号技術を勉強したことがある人ならばご存知だと思いますが、暗号通信を二者で行うときの例として出てくるのが常にボブとアリス。それが由来。
  • イブはEvil(邪悪な)から。
  • チャーリーはどこから来たのか不明。
  • フランシスコはサンフランシスコからか。
]]>
https://html5experts.jp/takoratta/20061/feed/ 0
モバイルWebのUIを速くする基本テクニックがわかる──Google I/O 2016 High Performance Web UI https://html5experts.jp/furoshiki/19276/ https://html5experts.jp/furoshiki/19276/#comments Fri, 08 Jul 2016 00:00:28 +0000 https://html5experts.jp/?p=19276 こんにちは、ふろしきです!

私はHTML5 Experts.jpで、過去2年ほどGoogle I/Oの情報を発信し、Web技術の変化についてお伝えしてきました。振り返るとGoogleは、2014年にモバイルWebの提唱と技術要素の拡大を図り、2015年からは「RAIL(モバイルWebが目指すべきパフォーマンス指標)」や「Progressive Web Apps(アプリのように振る舞うWeb)」といった、モバイルとの親和性が高いWebを作り出すための”考え方”を推し進めました。今年2016年は、さらにそれを踏み込んでいったという感じがします。

今回のI/Oで取り上げるのもそのひとつ。毎度お馴染みGoogle Developer AdvocateのPaul Lewis氏による 「High performance web user interfaces」です。彼は、モバイルにおいて、時にアプリのように振る舞うことが求められる昨今のWeb、すなわち「Progressive Web Apps」について、UIで起こりがちなパフォーマンス問題と、その改善方法について紹介しています。

31

※ この講演、動画無しでは説明が難しかったり、前提知識も多かったりするので、私でかなりアレンジ・要約して紹介しています。より詳細に内容を知りたい場合は、ソースをみることをオススメします!

Webは時として、モバイルアプリのような体験が求められる

モバイルにおいて、ホームスクリーンは重要な場所だ。人々はホームスクリーンから、目的を達成するためのアプリを起動する。Webは、Add to Homescreenを使うことで、ホームスクリーンからWebサイトへアクセスすることができるようになった。

するとどうなるか。このホームスクリーンをよくみてほしい。どれがWebで、どれがネイティブアプリなのかは見分けがつかないだろう。Google Mapsなんかはネイティブにみえるけれど、他はまったく想像がつかない。しかしこれらが、Google Mapsと同様にネイティブアプリにみえるなら、Webはネイティブアプリのように振る舞うことが求められている。

スクリーンショット 2016-06-06 23.14.36

パフォーマンスモデル、インタラクションモデルの2つによって、Webはモバイルネイティブアプリのような振る舞いをえることができる。Progressive Web Appsを実現することができる。今日はこの2つのモデルのうち、パフォーマンスモデルの話をしたい。

昨年は、Paul IrishIlya Grigorikなどの私のチームのメンバーが、「RAIL」というパフォーマンスモデルについて話した。RAILとは、Responseは0.1秒、Animationは16ミリ秒、Idleは50ミリ秒、Loadは1秒で動作すべきというもの。ただ、それを聞いた人々は、たまに勘違いをする。この4つの要素は、どれも全て、最も重要なこととして語ってしまうのだ。それは間違っている。

例えば、Webサイトにおいて、タップした時に求められるのは、4つの要素のうちLoadが重要になる。Idleが重要になることはそこまでない。そして、ホームスクリーンからタップして起動されるProgressive Web Appsでは、ResponseやAnimationが重要になる。Webサイトをつくるのと、Progressive Web Appsをつくるのでは、求められることが違う。

スクリーンショット 2016-06-11 17.19.02

さて、このようにパフォーマンス面で求められることが異なるProgressive Web Apps。そこに、3つのコンポーネントがある。Side Navigation、Swipeable Cards、Expand an Collapse。これらを実現するセオリーを紹介しよう。

1. Side Navigation

スクリーンショット 2016-07-03 22.27.14

まずは、このコンポーネント。メニューボタンをタップすると左からスライドインするバー。これは、2つのElementによって構成される。半透明の黒い背景と、サイドメニューを表示する領域だ。

スクリーンショット 2016-07-03 23.18.35

このサイドメニューの部分のCSSは非表示の時、CSSにpointer-events: none;を指定する。そして、表示されたタイミングでpointer-events: auto;を指定する。

そしてここからが大事な話。左から右、あるいは右から左に移動させる際に、transformを使う。ブラウザがDOMの位置を変更する際に、CPUを使ったレイアウト変更してはいけない。GPUの力を借りて、描画位置を変更することで、最適なパフォーマンスを得ることができる。

例えば、一昔前。サイドメニューが左に消えている時にCSSは

.side-nav {
  position:       fixed;
  left:           -102%; /* DOMのレイアウト位置を左にずらしてメニューを隠す */
  top:            0;
  width:          100%;
  height:         100%;
  over-flow:      hidden;
  pointer-events: none;
}

と、left: -102%で隠す。これは一般的な方法だった。しかし、描画を高速に処理できるGPUの恩恵を受けたいなら、transformを使って以下のように記述する。

.side-nav {
  position:       fixed;
  left:           0;                 /* DOMのレイアウト位置は常に0のまま */
  top:            0;
  width:          100%;
  height:         100%;
  over-flow:      hidden;
  pointer-events: none;
  transform:      translateX(-102%); /* 描画の位置を左にずらすことでメニューを隠す */
  will-change:    none;              /* <- これは何!? */
}

サイドメニューのDOMのレイアウト位置としては、x位置のleftもy位置のtopも、0のまま。横幅widthも縦幅heightも、100%ということで、全面を覆っているという扱いになる。しかし、transform: translateX(-102%);で描画の位置自体を、左に寄せている。

そして、ここで登場するのがwill-chanage: none;だ。

一昔前にtransform: translateZ(0);をCSSプロパティに指定して、パフォーマンスを改善するというハックが出回ったのをご存知だろうか。このCSSが指定されると、描画には必然的にGPUの力が必要になるため、強制的にGPUに描画を依頼することになる。GPUの恩恵を受けるために活用されたこのバッドノウハウは、will-chanage: transform;という新しいCSSプロパティをWeb標準として追加することによって、同様のことを実現できるようにした。(※注:実態はブラウザ対応の問題もあり、今もtransform: translateZ(0);を使うのが一般的)

ただ、transform: translateZ(0);will-chanage: transform;といったCSS指定は、常時ビデオカード上のRAMメモリーに描画結果をテクスチャーとして保存することになる。モバイル環境では、バッテリー消費などに悪影響を及ぼすことになる。動作するタイミングだけwill-chanage: transform;を指定し、動作しない時は無効化will-chanage: none;するといい。これが、バッテリー消費パフォーマンスと描画速度パフォーマンスのトレードオフ問題に対する、落とし所だ。

スクリーンショット 2016-07-04 0.37.11

黒背景については、will-change: opacity;というプロパティがあり、transformと同様の方法で、高いパフォーマンスで描画させることができる。(※ JSの実装については、「2. Swipeable Cards」にノウハウが似ているので割愛)

2. Swipeable Cards

スクリーンショット 2016-07-03 22.27.37

CSSを使ったパフォーマンス改善のテクニックの他に、注意しなくてはいけないのが、スワイプ操作時のコンポーネントの移動処理。ユーザーからの指の位置状況を入力し、それをスクリーン上に反映しなくてはいけない。この際、有用なのが「ゲームループ」のノウハウだ。

描画のイベントは常に、1/60秒ごとに発生する。対してスワイプのイベントは、常に一定には発生しない。描画のタイミングにはあわせてくれないのだ。

スクリーンショット 2016-07-04 1.05.35

そこで、スワイプにより発生するイベントについては、変数に位置情報だけを記録する。そして、描画時のイベントでは、記録された位置情報を元に、CSSを通じて描画位置変更をおこなう。

スワイプの開始時・移動時・終了時は以下の通り。this.startXthis.currentXthis.targetXといった変数に、現在の位置や、移動すべき位置を記録している。

/**
 * スワイプ開始
 */
onStart(evt) {

  // スワイプの開始位置を記録する
  this.startX = evt.pageX || evt.touches[0].pageX;
  this.currentX = this.startX;

  // cardの移動が開始されたことを記録する
  this.draggingCard = true;

  // will-change: transform; を有効にする
  this.target.style.willChange= ‘transform’;

  // カード上の要素にイベントを伝播させないように
  evt.preventDefault();

  // アニメーションを開始する
  requestAnimationFrame(this.update);
}

/**
 * スワイプ移動時
 */
onMove(evt) {

  // スワイプの現在地点を記録する
  this.currentX = evt.pageX || evt.touches[0].pageX;

}

/**
 * スワイプ終了時
 */
onEnd(evt) {

  // cardを削除すべきかどうか判定する
  let translateX = this.currentX - this.startX;
  const threshold = this.cardWidth * 0.35;
  if( Math.abs(translateX) > threshold ) {

    // cardの移動先をスクリーンの外へ(※cardは削除)
    this.targetX = (translateX > 0) ? this.cardWidth : -this.cardWidth;

  } else {

    // cardの移動先を最初の位置へ(※cardは削除されない)
    this.targetX = 0;

  }

  // cardの移動が終了されたことを記録する
  this.draggingCard = false;
}

描画のタイミングにrequestAnimationFrameから呼び出されるコールバックで、先ほどの位置情報を元に反映していく。

/**
 * 描画内容の変更
 */
update(evt) {

  // 次の描画タイミングでも自身を呼び出す
  requestAnimationFrame(this.update);

  // スワイプ中の場合
  if( this.draggingCard ) {

    // 現在の位置を描画させる
    this.translateX = this.currentX - this.startX;

  // スワイプが完了している場合
  } else {

    // カードを削除するかしないかに応じて指定の場所に能動的に移動する
    this.translateX += (this.targetX-this.translateX)/4;

  }

  // CSSプロパティを経由してGPUに変更を伝える
  this.target.style.transform = `translateX(${this.translateX}px)`;
}

(※ この後の処理については、「3. Expand and Collapse」にノウハウが似ているので割愛。)

3. Expand and Collapse

スクリーンショット 2016-07-03 22.27.51

タップすると、領域が広がり全体化されるUIコンポーネント。CSSではどうするのか?もちろん、ここまで説明してきた「transform」を活用する!では、JSについてはどうか?実は、「2. Swipeable Cards」とは異なり、スワイプ操作でなくタップによって、自動的にアニメーションする。この点で、より効率的な実装が求められる。

まず、アニメーションについて、動作中の状態はJS上で持たない。動作前後の状態だけを、CSSプロパティを通じてGPUに指示する。

スクリーンショット 2016-07-04 1.54.47

// 変化量を計算する
invert.x = first.left - last.left;
invert.y = first.top - last.top;
invert.sx = first.width / last.width;
invert.sy = first.height / last.height;

// 変化後の状態をCSSプロパティを通じてGPUに指示
card.style.transformOrigin = ‘0 0’;
card.style.transform =
    `translate(${invert.x}px, ${invert.y}px)
      scale(${invert.sx}, ${invert.sy})`;

そのままでは、タップした要素は一瞬にして全体化されてしまう。どのようにして何ミリ秒もかけて徐々に広げていくか?その方法は、CSSで指定する。JSではない。原理的には、従来よく使われているCSSアニメーションだ。

.cards {
  transition: transform 0.2s cubic-bezier(0,0,0.3.1); // アニメーションさせる
}

ここまで、Progressive Web Applsのパフォーマンス改善の話をしてきたが、「Google DevelopersのRendering peformance」が役に参考になる。一読するといいだろう。

スクリーンショット 2016-07-04 2.13.00

Progressive Web Appsのパフォーマンス改善。要はこう言いたかった

いかがでしたでしょうか?文字数の制限やコンテキストの高さもあり、多くのエンジニアに伝わるようかなりアレンジしてみましたが、ご理解いただけましたでしょうか?

Paul Lewis氏が言いたかったことは単純な話です。先ほどのGoogle Developersの記事にもありますが、Progressive Web AppsにおけるAnimationやReactionの課題は、いかにしてブラウザのレンダリング処理における「レイアウト」を減らすか、という話です。この講演は、そのTIPS集といえます。

スクリーンショット 2016-07-04 2.19.41

今日のノウハウ、特に新しいというわけでもなく2年前には既に実践されていたことです。実際のところ多くの現場では、OnsenUIやIonicのようなUIライブラリを活用することになり、このあたりの話を意識することはないのでしょう。ただ、Webのサービスを作っているフロントエンドエンジニアにとっては、ライブラリの有無に関係なく知っておくべき知識のように思えます。サイドメニューについては、Webサイトであっても鉄板のUIコンポーネントなので、Progressive Web Appsか否かはもはや関係ないノウハウだったに違いありません。

Webがモバイルに順応していくことは、今後もさらに求められていきます。これは、フレームワークやライブラリに限った話ではなく、トータルにみたWeb、フロントエンドへの要求に変化を与えるに違いません。

今後も、モバイルとWebの関わりに、目が離せませんね。

]]>
https://html5experts.jp/furoshiki/19276/feed/ 0
Webブラウザで高速な演算を可能にする低水準言語asm.jsと、WebAssembly詳解ーJavaScript が動く仕組み https://html5experts.jp/chikoski/18964/ https://html5experts.jp/chikoski/18964/#comments Thu, 07 Jul 2016 01:35:40 +0000 https://html5experts.jp/?p=18964 連載: 低水準言語asm.jsとWebAssembly詳解 (1)

Webブラウザの上で動作するアプリを書くための言語、といえば何が想起されるでしょうか。Flash、Sliverlight、Java、さまざまな言語が利用されてきましたが、やはり今のメインストリームはJavaScriptでしょう。

JavaScriptはさまざまな言語の特徴を併せ持つ動的言語で、Web技術の発展とAPIの整備の結果、Virtual Reality(VR)や画像認識、DAW(Desktop Audio Workstation)といった、少し前まではネイティブでの実装しかありえなかった種類のアプリケーションもWebブラウザをランタイムとするJavaScripで実装されるようになってきました。

そのようなアプリの代表例がゲームでしょう。少し前までのブラウザゲームといえば、リロードを繰り返すタイプのゲームか、Flashゲーム、パズルなどの簡単なものが大半を占めていたように思います。Canvasを利用して実装されたスーパマリオやNESエミュレータなどもありましたが、いずれも実験的なものであり、また20年以上前のハードウェアで快適に動くゲームだったことを考えると、CPU時間を大量に消費する「重厚」なものではありませんでした。

しかし最近は重厚なゲームの開発も行われ始めています。この嚆矢はBananaBreadでしょう。これはMozillaのエンジニアチームが開発した複数同時対戦可能なFPS(First Person Shooting)です。このようなゲームの実現が可能になったのは、WebGL、Web Workers、Web Audio API、Gamepad API、IndexedDBなどに代表されるAPIの充実もありますが、既存のJavaScriptエンジンではなしえなかった高速の演算を可能にする、低水準言語の整備のおかげでもあります。

この連載は4回にわたって、Webブラウザ上で動作する低水準言語であるasm.jsと、(いまのところ)そのバイナリフォーマットであるWebAssemblyについて、その設計と仕様、そして開発環境を紹介します。

低水準言語asm.js

asm.jsはMozillaが研究開発したJavaScriptのサブセットで、2013年に発表されました。現在はFirefoxとGoogle Chromeによって実装が行われ、EdgeやSafariも対応を表明しています。その特徴はなんといっても動作の高速さです。次のグラフはasm.js発表時に公開されたベンチマークの結果で、棒グラフが短ければ短いほど、処理が高速であることを意味しています。このグラフによると、ベンチマークの種類にも依存しますが、概ねCやC++によるネイティブ実装の半分程度のスピードで動作していることがわかります。

asm.js のベンチマーク結果。ネイティブの半分程度のスピードで動作している。 https://kripken.github.io/mloc_emscripten_talk/cppcon.html#/24より引用

このような高速に動作を可能にしているのは、事前コンパイルと呼ばれる技術です。AOT(Ahead of Time)とも呼ばれるこの技術を利用すると、プログラムはその実行直前にコンパイルが行われ、ネイティブコードへと変換されます。ブラウザは内蔵するコンパイラでasm.jsで書かれたコードをネイティブコードに変換し、ネイティブコードを実行することで、この高速性能を実現しているのです。

事前コンパイルを可能とするために、asm.jsで書かれたプログラムは以下にあげる特徴を持っています。

  • 変数や式、関数の型が静的解析可能である
  • 数値計算に特化している
  • 作成や属性の参照、メソッド呼出といったオブジェクトに関する操作ができない
  • 利用できるコレクション型はTyped Arrayのみである

一般のJavaScriptやAngular、RxJSといったモダンなフレームワークの提供するDSL(Domain Specific Language)に 慣れた身からすれば、機能が制限され、「低水準な」印象が拭えません。2013年にもなって、なぜ、このような制約の強い言語が開発されたのでしょうか。もちろんJavaScript発展の文脈に基づく、実用上の要求があるためです。それを理解するに、まずJavaScriptの動作について簡単に(かつ大雑把に)振り返ることとしましょう。

JavaScriptが動く仕組み

プログラミング言語は「コンパイラ型」と「インタプリタ型」の2つに分けることができます。C言語やC++は前者の典型で、 後者の典型はPerlやRuby、Pythonでしょう。JavaScriptも後者に分けられます。インタプリタ型の特徴は、あるプログラムの動作に「インタプリタ」と呼ばれる別のプログラムが必要である点です。SpiderMonkey(Firefox)、Chakra(Edge)、V8(Chrome / Node.js)はJavaScript向けのインタプリタとして有名でしょう。

インタプリタは、文字列を解釈してプログラムとしての文法的構造を取得します。この過程を字句解析・構文解析と呼び、 結果得られた文法的な構造のことを抽象構文木(AST: Abstract Sytactic Tree)と呼びます。例えばa=1+2*3; のASTは次のようになります。

a = 1 + 2 * 3;の抽象構文木

素朴なインタプリタは、この抽象構文木を枝の方から実行していきます。先ほどの例だと、まず2 * 3の計算を行い、 その結果である6で2 * 3に相当する部分木を置き換えます。その後1 + 6を計算し、最後にa = 6を計算します。この場合では、*や+、=といった演算子や標準ライブラリ中の関数などは、インタプリタ中の関数として実装され、それらをASTを解釈する巨大なswitch文の中から呼び出してプログラムは実行されます。そしてこの巨大なswitch文は、プログラムの評価が終わるまで繰り返し実行されます。

仮想マシンとバイトコード

このようなインタプリタはシンプルで理解しやすいのですが、変数のスコープや返り値の受け渡しの実現が困難になりがちです。そこで多くのインタプリタはASTをバイトコードに変換して実行します。バイトコードは単なるASTのバイナリ表現ではなく、インタプリタによって実装された仮想的なハードウェア(仮想マシン)を動かすマシンコード、つまり仮想的なネイティブコードとなっています。例えば(a,b,c)=>a+b*cという関数は、SpiderMonkeyによって次のようなバイトコードの列に変換されます。

getarg 0
getarg 1
getarg 2
mul
add
return
retrval

getargやmul、addなどはSpiderMonkeyの実装している仮想マシンの持つ命令です。仮想マシンには計算に使う値をスタックに保存するスタックマシンと、レジスタに配置するレジスタマシンとがありますが、SpiderMonkeyはスタックマシンを採用しており、変数への代入や、実引数の参照はスタックに対する操作として実現されています。

なおSpiderMonkeyの提供するバイトコードはこちらのサイトで一覧できます。

バイトコードに変わったとはいえ、実行のモデルは変わりません。バイトコードを1つずつとってきては、バイトコードを解釈する巨大なswitch文を通じて、各命令を実装する関数が呼ばれます。これがプログラムの実行が終了するまで繰り返されます。

型情報の不足

どうせネイティブコードを出力するなら、仮想マシンのネイティブコードではなく、実マシンのネイティブコードを出力すればいいのに。そう思われるのも当然ですが、出力しない、もしくはできないのにも理由があります。それらの中で大きなものの1つが、型情報の不足です。

function wrap(value){
  return {value: value};
}

var b1 = wrap(1);
var b2 = wrap(2);
var result = b1.value + b2.value;

resultにはどういう種類のデータが入るでしょうか?数値とすぐわかる方も多いとは思います。ではなぜ数値だとわかったのでしょうか?頭の中で上記のプログラムを実行した結果、b1.valueとb2.valueの値が両方とも数値であることがわかり、 数値同士の加算は数値になることから、resultには数値が代入されると結論づけたのではないでしょうか。

では、このwrapの返り値のvalue属性には常に数値が代入されているでしょうか?wrapの引数は数値でなければならない、とはどこにも書いてありません。そのため、次のような呼び出しも可能です。

var b3 = wrap("abcd");
var b4 = wrap({id: 1234});
var b5 = wrap(null);
var b6 = wrap(undefined);

b3,b4,5,b6それぞれのvalue属性の型も、string,object,null,undefinedと異なる型になっています。 このようにJavaScriptのプログラムは実行するまで変数や演算結果の型がわからないことが多々あります。 これはプログラムの中に型の情報が含まれていないためです。 もし型情報が含まれていれば、実行しなくても変数や演算の結果の型を決められます。 型付けの強い言語の代表例であるRustを使って同様のプログラムを記述すると、次のようになります:

struct Box<T>{value: T}
fn wrap<T>(value:T) -> Box<T>{
  Box{value: value}
}
fn main() {
  let b1 = wrap(1);
  let b2 = wrap(2);
  let result = b1.value + b2.value;
  println!("result = {}", result);
}

このプログラムでは実行しなくても、resultの型はintであることがわかります。resultの宣言からは、型宣言を省略してあります。それでもRustの処理系は他の情報から型を決定して型を決めています。それはBoxとwrapの宣言についている型情報、そしてwrapを呼び出した際の引数から、b1.valueとb2.valueの型が決定できるためです。

さて型がわからないことが、ネイティブコードの出力にどのような影響を与えるのでしょうか。それは端的にいえば、出力するネイティブコードが冗長になるということです。例えばIntelのCPUの場合、加算だけでも20種類以上の命令があります。 これはデータ型と、データの保存場所によって使用する命令が異なるからです。データ型が適切に決定できているなら、使用する命令を1つに絞ることができます。

しかしデータ型を適切に決定できない場合、その可能性を1つずつチェックし、そのチェックした結果に合わせて使用する命令を決めるといったようなコードを出力せざるをえません。その結果コード全体は冗長になり、スピードもあまりでなくなってしまいます。ネイティブコードの出力にも時間がかかるため、その時間に見合った効果が得にくくなってしまいます。

JITコンパイル

ネイティブコードを出力したいが、ソースコードには型に関する情報がない。この状況を打破するために利用されている技術がJIT(Just in Time)コンパイルです。これはJavaScriptをいきなり高速に動かすためのネイティブコードに変換せず、しばらくインタプリタなどで動作させます。変数に代入される値を観察して、その型に関する統計情報を集めます。

この統計情報と、1つの変数には同じ種類のデータが代入される傾向にある、というヒューリスティックを利用してその変数の型を推定していきます。推定がある程度できた時点で、該当するコードからネイティブコードを出力します。このようにプログラムを動かしながら、必要に応じてネイティブコードへと出力するのがJITです。

SpiderMonkeyではJITは2段階に分かれています。まずはnullチェックなどを含んだ冗長なコードを出力するベースラインJIT を行います。その状態でしばらく動作させ、型情報の統計を取得します。ある程度の型情報が集まったところで、その情報を元により効率の良いコンパイルを行います。

図中のIon compileがそれです。

SpiderMonkeyにおけるJITコンパイルの流れ。 asm.js AOT compilation and startup performanceより引用

型情報が集まったかどうかは実行回数によって決まっているようです。コードをざっと眺めた限り、10回程度繰り返し実行されるかどうかが、コンパイルを行うかどうかの判断の目安になっているようです。

上図のように、コンパイルされたコードは常に維持されるわけではありません。ときにはbail、つまりコンパイル結果を捨てて、もう一度型の推定からやり直します。これはJavaScriptの関数呼び出しに型による制約がかけられず、推定がヒューリスティックによるもののため、仕方がないことです。例えば、次のような呼び出しが行われた場合twiceのコンパイル結果は捨てられてしまいます。

function twice(a){
  return a + a;
}
var array = [0, 1, 2, 3, ... , 100000].map(twice);
var str = twice("こんにちは");

map関数からの呼び出しによってtwiceは10000回実行されます。この途中で(正確には、行われるかどうかは処理系に依存するのですが)、twiceはnumberを引数にとり、numberを返す関数としてJITコンパイルされます。しかし次の行の引数に文字列が与えられた呼び出しによって、その結果は捨てられてしまいます。捨てないとこの処理が行えないためです。

TypeScriptのような型制約があればこのようなことが起きないのですが、残念ながらJavaScriptにはそれがありません。 そのため時には時間をかけて行ったコンパイル結果を捨て、低速に動くことを余儀なくされてしまいます。

まとめ

JITを利用することで、よく利用されるコードを高速に動作させられるようになりました。 それでも次のような問題が残っています。

  1. よく使うコードしかネイティブコードにならない
  2. 高速に動作するようになるまでにはリードタイムが必要
  3. 型の推定には失敗することがある

重厚なゲームのようなアプリケーションの場合、2や3の問題は致命的なものとなりえます。

FPS(First Person Shooting)で対戦している場合を想像してみてください。対戦の最初はコンパイルがすんでいないため、ゲームはもっさりと動作しています。これではゲームになりません。しばらくは自分の陣地でゆっくりしてコンパイルが終わるのを待ちましょう。コンパイルが終わってFPS(Frame Per Second)が出てきました。ようやく本当のゲーム開始です。

そこで相手の攻撃を読み、侵攻して、草むらに潜み、相手がくるの待ち受けます。いざ相手へ攻撃をかけようとした瞬間、3に起因するコードの再コンパイルが発生し、画面がプチフリーズ。ゲームに復帰したら、目の前には銃を構える敵が…

こういう状況を避けるためにも、上述したような問題に対する回避策が求められました。それがAOTとasm.jsでした。AOTによって1,2の問題を回避し、その実現と3の回避のために型情報のふくまれたasm.jsが導入されることとなりました。

次回はasm.jsがどのように型情報を与えていくのか、型アノテーションを中心に解説します。

]]>
https://html5experts.jp/chikoski/18964/feed/ 0
Google Assistant、Android N、Daydream、Firebase…Google I/O 基調講演で発表された最新機能を一挙紹介! https://html5experts.jp/sakkuru/19832/ https://html5experts.jp/sakkuru/19832/#comments Wed, 06 Jul 2016 02:04:12 +0000 https://html5experts.jp/?p=19832 1706738f-6ac5-6eb3-6ac1-58f6139496ea

2016年5月18~20日の3日間、Googleの本社ビルのすぐそばにあるショアライン・アンフィシアターでGoogle I/O 2016が開催されました。

例年5千名程の参加者から大幅に増えて、今年は実に約7千人の人々が参加していたようです。屋外イベント施設ということもあり、まるで野外フェスのような雰囲気でした。

ca24a108-32c6-a0a8-8ada-9b041e73fb51

本レポートではGoogle I/O最初のセッションである、基調講演の内容について紹介します。

Google Assistant

4e8f5657-c3d5-2a6f-e88e-4697c0419a44

まず最初にスンダー・ピチャイ氏に紹介されたのが、Google Assistantです。

Google Assistantは新しい対話型のボイスアシスタント機能です。Google製のアシスタント機能といえばGoogle Nowがありますが、これを進化させ、対話型のシステムとしたのがGoogle Assistantのようです。コンテキストを認識し、ユーザーが質問すると現在地や直近のクエリーなどに応じて、適切に答えを返します。

IMG_6458

例えば、有名な建築物の前で「これを設計したのは誰?」と質問すると、その場所の情報に応じて回答してくれます。

デモでは、ユーザーが「今夜やってる映画は?」と聞くと、Google Assistantは近くの映画館で上映しているタイトルをいくつか提示し、さらにユーザーが「子どもと一緒に行きたい」と言うと、子どもと一緒に鑑賞するのにふさわしいと思われる映画を提案していました。さらに「4枚チケットが必要ですか?」とGoogle Assistantが尋ねてくるので、ユーザが映画のタイトルや枚数を答えると、自動で予約し、チケットの情報を提示する、というデモが行われました。

Screen Shot 2016-06-28 at 19.07.57

このように、『提案をしてくれる』というのがGoogle Assistantの大きな特徴です。Google Assistantは基調講演の中で発表されたGoogle HomeやGoogle Alloなどの製品にも組み込まれるようです。

Google Home

2e4a891d-0d45-9b7c-447a-7cff49acf55b

続いてGoogle Assistantの機能を組み込んだGoogle Homeという製品が紹介されました。

Google Assistantのボイスアシスタント機能の他にWifiスピーカーとしての機能もあり、クラウドから音楽のストリーミング再生もできるようです。プレイリストやアルバムへのアクセスもボイスコントロールでき、Google Castを通して、AndroidやiOSのデバイスから音楽を送信することもできるとのことでした。

Amazon Echoと似たような製品のようです。大きさは手のひらに乗るくらいの小さなデバイスでした。今年の後半から使えるようになるとのことです。

Google Allo

Screen Shot 2016-06-28 at 18.44.37

次にAlloというモバイル用のメッセージングアプリが紹介されました。見た目はLineやFacebook messenger等と大差なく見えます。

c6e3abf4-b7ae-efbb-7e80-867811a417c5

文字や絵文字の大きさを自由に変えることができることが紹介されました。地味だが良い機能、ということで会場が少し盛り上がります。

47f7f868-86d2-5fa4-fb9f-1593fa2db0dd

しかし当然のことながら、ただのメッセージングアプリではありません。このAlloにはGoogle Assistantの機能が組み込まれており、なんとチャットのテキストや画像を解析して、返信用のテキストを複数提案してくれます。

例えば「Dinner later?」とチャットの相手が送ると、「I’m in!」「I’m busy」という返信用のメッセージが表示されます。ユーザは選んでクリックするだけでメッセージを送信することができます。

またチャット内の画像も解析されるので、誰かが犬の写真をアップロードすると、犬種も識別された上で、「Cude dog!」「Aww!」「Nice bernese mountain dog」といった返信メッセージが提示されます。

またGoogle Asisstantのbotががチャットの中に存在するようなかたちになっており、店のお店のサジェストや予約、検索等も対話形式でチャットの中で行うことができるようです。

さらにChromeのようにIncognito Modeが搭載されており、エンドツーエンドでの暗号化やNotificationの制御、メッセージの期限などを設定できるようになっているとのことです。

Google Duo

3651367c-53da-41c7-08ff-33b848f3657e

続いてDuoというビデオチャットアプリの紹介です。

ビデオチャットを実現するアプリは数多く世に出ていますが、このDuoにはビデオコールをかけられた側は、応答する前にそのビデオストリームを見ることができるという特徴があるようです。この機能は『Knock Knock』と呼ばれているそうで、これにより着呼側は誰が、どんな状況でコールしてきたのかを知ることができます。

コール先は電話番号に紐づくとのことで、AppleのFaceTimeの競合として考えられそうです。

9cf474f9-02e2-7500-3c8d-b5b976175073

DuoはWebRTCとQUICを使用したWebプロトコルベースのアプリと紹介されました。

AlloとDuoはAndroidとiOSで2016年夏頃リリースされる予定で、Android版では事前登録が開始されています。

Android N

続いてAndroid Nについての発表が行われました。

GoogleがAndroidの開発を行うようになってから、10年が経過したようです。今やAndroidはスマートフォンだけではなく、Android Wear、Android TV、Android Autoなど、多くのプラットフォームが登場しています。

a34e8828-9bcd-5887-8f9f-649ee8b38bc2

Androidの最新バージョンであるAndroid Nは現在プレビュー版(6/28現在Developer Preview 4)が公開中です。こちらで名前の募集も行われていましたが、現在は終了し近日中に公開されるようです。

Android Nでは、新しいOSのアップデートがあると、自動的にソフトウェアをバッググラウンドでダウンロードし、次回Androidの電源を入れた際にシームレスで新しいソフトウェアイメージに切り替わるようです。

またマルチウィンドウが正式に採用され、スマートフォンでYoutubeを見ながら他のタスクを行ったり、Android TVでは映像を見ながら検索などの他の作業を行うことができるようになります。

現在はAndroid NのDeveloper Preview 4がリリースされており、Nexus 6やNexus 5X等の一部の対応した機種であれば、Android Nを試用することができます。(Android Developers Blog)

Daydream

続いてI/O開催前から噂が絶えなかったVRに関する発表です。 a7ac330c-c1bc-6d81-4051-6125b966045d

Androidスマートフォンで高品質なVR体験を提供可能であるDaydreamというプラットフォームが発表されました。

どのようにAndroidスマートフォン上で、高品質なVR体験を可能にするのか、講演では3つの要素が挙げられていました。 まずはスマートフォン本体、次にヘッドセットコントローラのリファレンスデザイン、そしてアプリで、ぞれぞれが協調してエンドツーエンドのユーザ体験を提供するために設計されています。

ハイパフォーマンスセンサーやヘッドトラッキング、表示のレスポンスの速さなど、VR用に作られた仕様を満たしたスマートフォンは、Daydream-readyと呼ばれ、高いクオリティのVR体験を提供することができます。Daydream-readyなスマートフォンは、SamsungやAlcatel、Asusなどの企業から、2016年秋以降リリースされるようです。発表された中に日本の企業はありませんでした。

334aaad1-e834-a534-12b4-63c89e6ab9f9

リファレンスデザインとして紹介されたヘッドセットとコントローラは非常にシンプルなものでした。コントローラはボタンは少なくクリッカブルなタッチパッドがついており、スクロールやスワイプができます。オリエンテーションセンサーも内蔵しており、ユーザがどこをポインティングしているかが分かります。

Screen Shot 2016-06-28 at 18.49.36

アプリに関しては、VR用のGoogle Playが開発されており、ユーザーがアプリを探し、購入、インストールがVR上で可能になることが発表されました。またGoogle Play MoviesやGoogle Mapsのストリートビュー、Google Photos、YouTubeもVR上で鑑賞できるようになります。

Daydreamが使えるようになるのは2016年秋になるとのことですが、Android NのDeveloper Previewを使用することでアプリなどの開発は今から行うことができます。

Android Wear 2.0

18168a75-4f59-d2b0-b6fc-9f86caf03b6d

Android Wearは2年前のGoogle I/Oで初めて発表されましたが、今回は初めて大幅なアップデートとなるAndroid Wear 2.0が発表されました。

大きな変化としては、今までAndroid Wearのアプリがインターネットにアクセスするためには母艦となるAndroidやiOSスマートフォンが必要であったことに対し、Android Wear 2.0ではスタンドアロンで動作することが可能になることが挙げられます。アプリが直接BluetoothやWi-Fi、LTEなどを通してインターネットにアクセスできるようになり、ペアリングされたスマートフォンが電源を切っている場合でも、アプリは引き続きフル機能を提供できるようになります。

その他にもUIの刷新や、手書き入力の対応等も発表されました。

Android Wear 2.0はプレビュー版が公開されおり、秋には正式リリースとなるようです。

Progressive Web AppsとAccelerated Mobile Pages

基調講演も終盤に差しかかり、ここで初めて、わずかですがWebについての発表がありました。 モバイルデバイス上でWebをより快適にするための取り組みとして、Progressive Web AppsとAccelerated Mobile Pagesの2つの紹介です。

Screen Shot 2016-06-28 at 19.01.42

Progressive Web Apps、略してPWAppsは、オフライン状態でも動作可能、エンゲージメントを高めるための通知、ホームスクリーンにアイコンを登録、といったネイティブアプリのような振る舞いを実現したWebアプリのことを指します。ChromeにはServiceWorkerをはじめとする、Progressive Web Appsを実現するための仕組みが実装されています。

Screen Shot 2016-06-28 at 19.01.55

Accelerated Mobile Pagesは、既存のWeb標準に基づいた非常に速いモバイルWebサイトを作るための、オープンソースのプロジェクトです。

特に新規発表はなく、基調講演内ではわずか1分程度の紹介でしたが、Google I/O全体では多くのPWAppsやAMPのセッションが行われていました。HTML5 Experts.jpでもGoogleが新たに提唱するProgressive Web Appsの新たな開発パターン「PRPL」とは?という記事を公開していますので、そちらをご覧ください。

Android Studio

Android Studio 2.2 Previewが公開されたと発表がありました。

Screen Shot 2016-06-28 at 19.03.28

Android Studio 2.2では、ビルドが10倍、エミュレータも3倍速くなっているそうです。また、新しいレイアウトデザイナやAPKアナライザーが搭載されるとのことでした。

Firebase

続いてFirebaseの新バージョンが発表されました。

1f7e99f1-eb19-5576-3d64-48edebab815b

FirebaseはGoogleが2014年の10月に買収したBaaSサービスです。モバイルアプリに特化した分析ツールである、Firebase Analyticsが発表されました。ユーザがアプリ内で何をしているか、ユーザがどこからきたのかなど分析することが可能です。Android AnalyticsはAndroidでもiOSも扱うことができ、無制限に無料で使用することができると発表されました。

Android Instant Apps

adcac44d-79e3-0921-0316-3e9bf50fc99e

今回の基調講演の後半で一際注目を集めたのがこのAndroid Instant Appsではないでしょうか。

Webアプリの場合、ユーザはリンクをクリックするだけでアプリを使用することができますが、Androidアプリはインストールを必要とするため、ユーザがそこで離脱してしまうことが多くあります。その課題を解決する一手となるのが今回発表されたAndroid Instant Appsのようです。

今回発表されたAndroid Instant Appsでは、AndroidアプリがWeb等のリンクからインストールレスで使えるようなるとのことです。

講演中のデモの中では、いくつかのAndroidアプリをURLから起動し、実際にアプリを使用できることが示されていました。 アプリの中の直近で必要になるモジュールだけをフェッチしているらしく、起動も素早く行われ、アプリを使用してからのインストールも非常にスムーズに行えるようです。

これにより、Androidアプリ開発者は、より多くのユーザにリーチできるようになります。AndroidアプリとWebとの親和性が高くなり、Androidアプリのユーザ体験が大きく変わる機能となるのではないでしょうか。

おわりに

今年はGoogleのAIの技術力を発揮した製品やサービスが多く発表されました。

基調講演内でWebやChromeに関する発表は非常に少なかったのですが、Google I/O全体では33ものモバイルWebに関するセッションが行われていました。

既に公開されている

以外にもセッションレポートを公開していく予定です。お楽しみに。

]]>
https://html5experts.jp/sakkuru/19832/feed/ 0
Firefox 47・Safari 10、EdgeとProgressive Web Appsの新機能など─2016年6月のブラウザ関連ニュース https://html5experts.jp/myakura/19921/ https://html5experts.jp/myakura/19921/#comments Tue, 05 Jul 2016 02:25:46 +0000 https://html5experts.jp/?p=19921 連載: WEB標準化動向 (14)

6月7日にリリースされたFirefox 47や、WWDCで発表されたSafari 10、WindowsにおけるProgressive Web Apps対応など、6月も注目のブラウザニュースをお届けします。

Firefox 47リリース

6月7日にFirefox 47がリリースされました。

今回は開発者ツールを取り上げましょう。まずはResponsive Design ModeでUA文字列をカスタマイズできるようになりました。いまのところプリセットがないので面倒ではありますが、そこそこ便利です。

スクリーンショット:開発者ツールのResponsive Design Modeのツールバー。Custom User AgentでUA文字列を設定できるようになった。

さて、これはいいなと思ったのが、コンソールの複数行入力です。以前からShift+Enterで可能でしたが、Shiftを押し忘れてしまって赤い SyntaxError を見るなんてひとは多かったのではないでしょうか。

スクリーンショット:Chrome DevToolsのコンソールで、複数行入力をしようとして失敗した例。「{」でEnterを間違えて押してしまいエラーがでた。

Firefox 47の開発者ツールは入力中のコードの構文を解釈するようになり、{ など「開いている」状態でEnterを押すと自動的に複数行入力になります。foo(...) など「閉じている」場合はふつうに評価されます。

スクリーンショット:Firefoxの開発者ツールのコンソールでは、構文を解釈するので「{」は開いた状態と認識され、ただのEnterでも次の行にカーソルが移動する。

構文エラーを意図的に入力したい方には不便かもしれませんが、基本的にはとても便利です。
ちなみにSafariではたしかSafari 7くらいからできます。

ほか、Service Worker関連のデバッグに便利な機能が多数追加されています。

Safari 10発表

6月13日から17日までAppleのWWDCが開催され、MacのOSがOS XからmacOSになるなどいろんなことが発表されました。
SafariもSafari 10になることが発表されました。すでに実装される機能が公開されています。

ES6のサポートはうれしいですね。Indexed DBやCSS Filtersの接頭辞削除など、互換性も向上しています。
フォント関連機能にも大きな進展があります。Font Loading APi、WOFF 2.0、unicode-range のサポートが追加されました。とくに日本語のWebフォントは容量が大きいので、Webフォント関連の機能向上はうれしいですね。

このエントリでは、先日Webフォントの読み込み中の挙動が変更されたことも書いてあります。これまではWebフォントが読み込まれるまでテキストが表示される問題でしたが、今回の変更でFirefoxやChromeと同じく、Webフォントの読み込みに時間がかかる場合は3秒後にフォールバックのフォントで一旦表示するようになりました。Safari 10の紹介には書いてありませんが、この変更が取り込まれてることを祈りましょう。

Keynoteで発表されたApple Pay on the WebについてはSafariでのみしか利用できない、Web Paymentsワーキンググループで策定している標準APIではないといった批判の声もあります。これについてはAppleのEdward O’ConnorがWeb Paymentsワーキンググループに対して説明をしています。

マーチャントの検証機能や canMakePayments() といった標準にはないメソッドがApple Payにはあるそうです。AppleもWeb Paymentsワーキンググループに参加していますし、標準APIもよいものにしていってほしいですね。

また、Flashコンテンツについても、ユーザーのアクションなしには実行できないようになると発表されました。

MacにはFlash Playerがプリインストールされていないので、インストールしていない人もいるかと思います(Chrome内蔵のFlash Player経由で見るなどは大いにありそうですが)。他のブラウザーもFlashをなるべく実行しないように動いていますし、HTML5なコンテンツへの移行が急務になりそうですね。

EdgeとProgressive Web Apps

Microsoft EdgeチームのJacob Rossiが、MediumにWindowsにおけるWebアプリのこれまでと今後のProgressive Web Apps対応について投稿していました。

WindowsにおけるProgressive Web Apps対応は、ChromeやOperaのそれよりも、よりOSの機能との統合を考えているようです。具体的な例としてアプリケーションのリストへの追加や、アプリの設定やアンインストールといったものを紹介しています。また、WindowsストアやBingの検索結果への表示といった、アプリの発見についてもいろいろ考えている模様。PWAをふつうのアプリとして扱うよという強いメッセージを発しています。

この記事と時を同じくして、Push API, Web App Manifest, Cache APIの実装開始も発表されました。Windowsというデスクトップ環境が主流のプラットフォームでWebアプリをどう統合していくか、とても楽しみです。

]]>
https://html5experts.jp/myakura/19921/feed/ 0