RecordRTCを使ったデスクトップ画面共有アプリを作ってみた。

前回記述した
WebRTC(PeerJS)とPolymerを使ったデスクトップ画面共有アプリを作ってみた。
の派生として、
RecordRTCを使ったデスクトップ画面共有アプリを作ってみました。

こちらからインストールできます。
(とりあえず動くものをアップした状態なので、画像とか使い方が書けてないですが。。)
◆ Desktop Capture Recorder
https://chrome.google.com/webstore/detail/desktop-capture-recorder/boajadhokhjkmfjmnmmkkmgnafnbcfco

このアプリが何ができるかというと、デスクトップで開いているアプリケーションを録画し、録画内容のプレビュー・保存を行うことができます

前回記述したアプリではデスクトップ画面のリアルタイム共有ができますが、セキュリティやネットワークのトラフィックなど気になる箇所がありました。
これで問題解決です。。たぶん。

・使い方をザックリと。
1. 拡張機能のアイコンを押す。
2. デスクトップ画面のどの画面を共有するか選ぶ。すると録画開始。
3. 拡張機能のアイコンを押す。すると録画停止。
4. 拡張機能のアイコンを押す。吹き出しが出てくるので、プレビュー見るか保存するか削除するかを選ぶ。
以上。

作り方

要領は前回と一緒で、getUserMediaで取得したstreamを、RecordRTCに流し込むだけ。
簡単です。
シグナリングサーバーも外部への通信も何も入らないので、更に楽。

・RecordRTC
https://www.webrtc-experiment.com/RecordRTC/

まとめ

簡単です。って書きましたが、ハマったところもありました。
何も考えずにgetUserMediaで取得したstreamを、RecordRTCに流し込むと、録画した動画の縦横比率がめちゃくちゃになりました。

どうしたかというと、
1. stream取得
2. videoタグを作成しstreamを流し込む
3. videoタグの[canplay]のeventでvideoWidth/videoHeightを取得する
4. 「3.」で取得したwidthとheightをRecordRTCのオプションに指定する

ということをしました。

要望があればソースも混じえて解説するかもしないかも..です。

以上!

WebRTC(PeerJS)とPolymerを使ったデスクトップ画面共有アプリを作ってみた。

表題の通りですが、
WebRTC(PeerJS)とPolymer(全然凝ってないけど)を使ったデスクトップ画面共有アプリを作ってみました。

これで何ができるかっていうと、
画面上で起動しているアプリケーションをリアルタイムに画面共有ができます。

図. こんなイメージ。


共有する側(図の左側)はChrome拡張機能をインストールする必要があります。

これは、「Desktop Capture API」を使いたかった為です。
chrome.desktopCapture
⇒ https://developer.chrome.com/extensions/desktopCapture

共有を開始すると共有用URLが生成されるので、
こちらを閲覧する側(図の右側)に教えてGoogle ChromeでURLを開くだけでリアルタイムに操作内容が閲覧できます。

◆ Desktop Capture Share
http://screen-share.itinao.asia/

◆ Chrome拡張機能インストール
https://chrome.google.com/webstore/detail/desktop-capture-share/albmbgococdgmmoajacmcbfcidhbinlh

使った技術

Chrome拡張機能側
Knockout.js
⇒ 状態によって表示するものが切り替わるので、フレームワークを使用しました。
無くても書けましたがソースが汚くなっていくー。。って感じだったので導入。

共有されるURL側
Web Components(Polymer)
⇒ 試しに使ってみました。
peer-videoタグを作ったぐらいです。
(属性でホストとかURLを指定すれば勝手にシグナリングしてvideoタグで再生してくれるやつ。)

共通
WebRTC(PeerJS)
⇒ ほんと楽にシグナリングしてくれるんで便利。
(PeerJSサーバーも立てました。)

gulp
⇒ タスクランナーはgulpを使ってみました。
Polymerを使ったので、gulp-vulcanizeを導入してロードファイル数を下げてみる。とかやりました。

まとめ。

これは、技術的にできるなーって思ってから1〜2日で動作の確認までできました。
見た目整えて、gulp走らせるようにして、、ってやってたら1週間以上かかってのリリースになりました。

あと、
RecordRTCを使って録画機能も付けてみようかなーと考え中です。

録画したWebMファイルを接続相手に送信。。なんてこともできちゃいますね!
なんか色々出来て面白い。

以上!

Shadow DOMについて。

今回はShadow DOMの「content」と「select="セレクタ"」と関連するセレクタらへんの話しにします。

Polymerを勉強したての頃は「?」な部分が多く、

何がよく分からないのかなーと色々と調べたところ、
Web Components(特にShadow DOM)の理解が足りなかったんだなってことが分かりました。
(きっといきなりPolymerを覚えると、jQueryは触れるけどJavaScriptよくわかんないみたいな感じ?
なので、今回はShadow DOMがテーマです。

今回のサンプルコードは下記のAPI使っています。
HTML Templates
Shadow DOM

筆者の実行環境は【Google Chromeのバージョン 37.0.2062.124】です。
(Shadow DOM は Chrome 35 以降で利用可能らしいです)

Shadow DOMって?

以前の記事「Web ComponentsとPolymerをさわってみる。」
にも記述しましたが、
【DOM要素のレンダリング結果を、その要素が持つDOMのサブツリーとは独立に与える仕組み。】です。

ページの他の部分から切り離され、CSSの記述を間違って同じクラス名つけちゃっても、個々に解釈されるため大丈夫。って感じです。
その他にも色々機能があるので、サンプルコードで説明していきます。

サンプルコードで見てみる

基礎編
1. 通常パターン
DIVにスタイルをあてると、当然こうなる。

2. Shadow DOMを使って、見た目の情報を隠す。
要素に生えている「.createShadowRoot()」を実行すると、見た目の情報はShadow DOMを見に行くようになる。
(今回の場合、Shadow DOMに何も要素が入っていないので、外側で定義されているborder等のみ表示されている。

3. Shadow DOMに要素を入れていみる。
「.createShadowRoot()」で生成されるオブジェクトに対して、要素を入れると表示される。

4. いちいちcreateElementなんてやってらんないよ!ってことで、HTML Templatesを使う。
templateタグで要素を作成し、JavaScriptでAppendする。


Content編
5. contentタグを使って、「<div id="test1">test1</div>」で記述してた「test1」というテキストを復活させる。
上記のサンプルで表示されなくなっていた「test1」(言い換えると、Shadow DOMを用いてる要素のinnerHTML)は、contentというタグを挿入ポイントとし、代入してくれる。


6. contentタグを複数使ってみる。
contentタグに「select="セレクタ"」を指定することで、挿入する内容を指定することができる。(指定していない場合も混合できる)
※注意点: selectは直下にある要素しか選択することができない。こういう指定はできないってことです⇒[select="ul li"]


スタイル編
7. 【:hostセレクタ】Shadow DOMの「ページの他の部分から切り離される」というスタイルの当て方をする。
DIVにスタイルを当てていましたが、これをTemplate内へ移動します。
そして、「:host」というセレクタを指定することで、Shadow DOMのホスト要素に対して、Shadow DOMからスタイルを当てることができます。
また、「:host(.hogehoge)」と指定すると、ホスト要素にhogehogeクラスが指定されてた場合に適用されます。
(外側からスタイルを指定している場合は、外側が優先されるので注意。)


8. 【:host-contextセレクタ】Shadow DOMのホスト要素の状態でスタイルを切り替える。
id="test2"に対してもレイアウトを適用したいので、shadowDomTestという関数を作り、test1とtest2の両方にShadow DOMを適用しました。
しかし、サンプルのソースコードではtest2のみスタイルがあたっています。

これはtest2の要素の親にclass="hogehoge"と指定した為で、
「:host-context(.hogehoge)」と指定すると、Shadow DOMのホスト要素の親要素にhogehogeクラスが指定されてた場合に適用されます。
(:hostより優先度が高いので注意。)


9. 【::shadowセレクタ】Shadow DOMの外側からスタイルを当てる。
template要素内に「<span>test0</span>」を追加し、外部から「#test2::shadow span」でスタイルを指定すると、「<span>test0</span>」に適用されます。


10. 【::contentセレクタ】contentタグで挿入された要素に対してスタイルを当てる。
id="test1"の子要素の「<span hoge2>test1_2</span>」は、template要素内のcontentタグに代入されます。
content内の要素に対してスタイルを当てる場合は「::content [hoge2]」を指定します。


11. 【/deep/セレクタ】 Shadow DOMのカプセル化を突き破ってスタイルを指定する。
「body /deep/ span」このように指定することで、「::shadow」と書いていた部分もスタイルの適用可能箇所となります。
(Shadow DOM以外の要素にも適用されます)


まとめ

このサンプルコードに記載したことを把握しておけば、PolymerのDemoコードも少しは読めるようになるはず。。と思います。
が、Polymer独自の拡張部分である、Layout属性だったり、{{ 値 }}みたいな双方向バインディングの仕組みをもってたりするので、
それはそれでドキュメントをシッカリ読まないと理解できないかもしれません。

以上!

Web Componentsを試してみる

【Web ComponentsとPolymerをさわってみる。】の続きですが、
今回はWeb Componentsで軽くサンプル作りつつポイントを書いていきたいと思います。
Polymerについては触れないです。

今回サラっと触っているのが、下記のAPIです。
Custom Elements
HTML Imports
HTML Templates
Shadow DOM

ちなみに、筆者の実行環境は【Google Chromeのバージョン 37.0.2062.120】です。 

サンプルコード

このコードは、ホバーすることで色の変わる【x-sample】というタグを作っています。
そして、コード中にある、8つの【※注目※】については下部で説明します。


  • 注目1
:unresolved
というCSSの擬似クラスが適用されるのは、Custom Elementsが未定義の場合。
サンプルコードを実行すると、<x-sample>は適用されておらず、<x-sample2>は適用されている。
<sample>は命名規則からズレているからか、適用されず。
「:unresolved」の使いドコロは、未定義状態の要素が見えてしまうことによって起こる表示のチラツキを抑えるとかになります。

  • 注目2
<link rel="stylesheet" href="sample.css">
でcssを読み込もうとしているが、読み込みされない。
http://www.w3.org/TR/shadow-dom/#inert-html-elements
図. 通信さえ走らない
直書きすれば問題はないが、それでは色々と辛いので下記のような手法がある。
Vulcanizeで減らすHTML ImportsのHTTPリクエスト

また、Polymerの場合は良しなにインライン展開してくれるらしい。

  • 注目3
ここでdocumentを参照すると、インポートした側の参照になってしまう。
インポートされる側を参照できるようにしておく。

  • 注目4〜7
4〜7で指定しているコールバックの実行タイミングは下記です。
実際に、Developer Toolsのconsoleから実行してみればよく分かると思います。
// 要素が作成されたタイミング
var sample = document.createElement('x-sample');
⇒ createdCallback 
// 要素の属性が変更されたタイミング
sample.id = "sample";
⇒ attributeChangedCallback 
// 要素がDOMツリーへ挿入されるタイミング
document.body.appendChild(sample);
⇒ attachedCallback 
// 要素がDOMツリーから削除されるタイミング
document.body.removeChild(sample);
⇒ detachedCallback

  • 注目8
Custom Elementsで使用できるタグの名前には、ハイフンが必須です。
下記の場合にはこんなエラーに。
document.registerElement('sample')
SyntaxError: Failed to execute 'registerElement' on 'Document': Registration failed for type 'sample'. The type name is invalid.

終わりに

Web Componentsネタは、ちょいちょい書くかもです!

以上!

JavaScriptでファイルを即時ダウンロードさせる処理を作ってみた

表題のとおりですが、詳細はこんなんです。
1. WebRTCでPeer to Peerする
2. 自身のブラウザで画像ファイルとかをドラッグ&ドロップする
3. 自身のブラウザでそのファイルを送信処理をする
4. 接続している相手のブラウザで勝手に即時ダウンロード
(ブラウザの設定で、保存ディレクトリを固定だったら勝手に保存される)
↑これ、結構ひどいことできますよね。。

ということで、
この記事はWebSocketやWebRTCを使って他人と接続し、他人からのPUSHを受け取れる状態になっておくことが前提になります。

で、どうやるの?

自身がPUSHして、相手側の受信イベントのコールバックでこんなスクリプトを実行するだけです。
// dataの内容は下記
var filename = data.filename;
var blob = new Blob([data.file], {type: data.mimeType}); 
// ファイルを自動でダウンロードする
var element = document.createElement("a");
element.download = filename;
element.href = window.URL.createObjectURL(blob);
element.click();
この受信処理で、PDF・PNG・GIFは問題なくダウンロードできたのは確認しました。

これだけではアレなので、筆者の場合…↓

筆者の場合 その1(送信ファイルの準備)

上記コードの、「data」の内容は下記です。

ドラッグ&ドロップできるdivを用意し、ファイルをドロップする。
そのdropイベントで、受け取ったファイルの内容が「data」です。

dropイベントのコールバック
var files = event.dataTransfer.files;
for (var i = 0, l = files.length; i < l; i++) {
  var file = files[i];
  // ★★★data★★★↓
  var data = {
    file: file,
    filename: file.name,
    mimeType: file.type
  };
}

筆者の場合 その2(送信処理)

筆者が作ってみた環境では、PeerJSを使いました。
前回はライブラリ使わずに色々やりましたが、これはめっちゃ便利でした…。
dataConnection.sendってやるだけで、JSONやバイナリBlobs、ArrayBuffersを良しなに送れます。
(PeerJSはBinaryPackシリアライゼーションフォーマットを利用しているらしい)
http://nttcom.github.io/skyway/docs/

ということで、上記の「data」は dataConnection.sendで送っているだけです。
※ connection時のオプション指定で、serializationを「binary」にするのをお忘れなく。

おわりに

aタグにdownload属性なんてあったんだっていう発見ができました。
そして、PeerJSの便利さも覚えてきたので、これらを使って何か作りたいと思ってます。

以上!

JavaScriptのメモリ消費について

今回は、メモ書き...

テーマはJavaScriptのメモリ消費についてです。

JavaScriptのメモリモデルはガベージコレクションという技術を用いています。
この手法は、ガベージコレクター(以下、GC)が最適と判断したタイミングでメモリの開放を行います。

問題点としては、下記があげられます。
・タイミングが制御できない
・GCの処理中は、プログラムが利用可能な処理時間を奪う

要は、メモリ消費が大きいWebアプリはGCが頻発し、パフォーマンス劣化に繋がるよってこと。

GCの頻度はどうやって確認する?

下記の画像は、YahooのトップページをDeveloper ToolsのTimelineタブでレコーディングしたものです。
山になっている水色部分がメモリの使用量で、一回ガクッと下がっています。
これがGCのタイミングで、ギザギザになってるほどGCの頻度が高いです。
(「バージョン 39.0.2159.0 canary (64-bit)」の画面です。)
図1. Developer ToolsのTimeline
ちゃんと確認したいのであれば、「GC」でフィルタするとことによっていつGCが発生したのかが確認できます。
図2. Developer ToolsのTimeline

GCの対象は?

JavaScript内ではすべての値がオブジェクトグラフに属しており、グラフにはルートが存在します。
そのルートからの参照がなくなった時点で、値はガベージコレクションの対象になります。

(そして、GCの処理中はオブジェクトグラフがルートから順に走査される間はページのスクリプト実行は中断されるって感じになる。)

GCの頻度を下げるにはどうしたらいいか?


  • メモリリークについて理解する

これについては説明が長くなりそうなので、下記の記事が非常に参考になります。
要は、不必要なクロージャに使用を避ける&循環参照を避けるってことがポイント。

【JavaScript】メモリの浪費を避けるコーディング
http://utage.headwaters.co.jp/blog/?p=1116

JavaScript アプリケーションのメモリー・リークを理解する
http://www.ibm.com/developerworks/jp/web/library/wa-jsmemory/


  • 大量のオブジェクトを作成/破棄することをなるべくしないようにすればよい

スタティックメモリJavaScriptというテクニック
(があるらしい。こういう実装したことないので知識だけ書いておきます。)

1. 同じ型を持つオブジェクトを必要な分だけセットして保持しておく、オブジェクトプールと呼ばれるものを作る。
2. 新しいオブジェクトが必要となった場合、システムのヒープメモリから取ってくるのではなく、プール内の使用されていないオブジェクトを再利用する。
3. 不要になったオブジェクトは、メインメモリではなくプールに返却する。

つまり、最初にオブジェクトをゴソッと作って、必要な場合はゴソッと作った中のものを使う。

【メリット】
フレーム毎に生成/破棄されるオブジェクトの数を必要最低限に抑えることができる

【デメリット】
初期化時の挙動が重い
メモリをあまり使用していないときも、使用メモリ量は減らない

(その他..)
また、
'hoge'.slice() とか 'hoge'.substr() とか非破壊関数は新しく配列・文字列を作って返すので注意すべし。

終わりに

ここに書いたことは、
担当している案件で動作が遅いとか、パフォーマンスチューニングして?って言われたときに気にするポイントの1つです。

ネットワーク部分以外のチューニングとしては、他に描画処理のパフォーマンスチューニングやJavaScriptのコード最適化(JSXみたいな)などがあります。
今後記事にできたらいいなーと思います。

以上!

参考

オブジェクトプールを使った静的メモリ JavaScript
http://www.html5rocks.com/ja/tutorials/speed/static-mem-pools/

Gmail スケールの効率的メモリ管理術
http://www.html5rocks.com/ja/tutorials/memory/effectivemanagement/

Web Speech APIを使ってみる

Google ChromeでJavaScriptを使った音声認識ができるので、試してみました。
Web Speech APIというのを使いますが、これは結構前から実装されています。

仕様はこちらに載っていますが、W3C標準ではありません。
https://dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html

各ブラウザへの実装状況はこちらです。(赤はサポートしてない)
図. Web Speech API対応状況
※ 最新はリンクから確認してください
http://caniuse.com/#feat=web-speech

サンプル

このAPIを使うと、音声解析とテキストの読み上げを実現することができます。
サンプルは下記のコードです。(楽だったのでKnockout.js)

使い方(今年、1回でもアップデートしてるGoogle Chromeなら動くはず)
1. 「録音開始」ボタンを押す
2. マイクへのアクセス許可をします(ページ上部に確認が出る)
3. 「開始前」から「録音スタート」と文言が変化するので、適当に喋ってみる
4. 「録音開始」ボタン下部に喋った内容と「読み上げる」というボタンが表示される
5. 「読み上げる」ボタンを押すことで、喋った内容が読み上げられます
※ 間隔を空けながら喋れば、喋った内容の文字が繋がらず、下にどんどん追加されます。

ちなみに…

var recognition = new webkitSpeechRecognition();
recognition.start();
上記を実行した後のイベントは、下記で制御できます。

onstart: 音声認識サービスのスタート時に発生。
onend: 音声認識サービスが切断したときに発生。onstart後に発生するイベント。
onaudiostart: オーディオをキャプチャするために開始したときに発生。
onaudioend: オーディオをキャプチャを終了したときに発生。onaudiostart後に発生するイベント。
onsoundstart: いくつかの音が検出されたときに発生。クライアント側の検出器を用いることによって、低レイテンシで生成される。(静かな環境で何も喋らないと、発生しないイベントでした)
onsoundend: 音が検出されないときに発生。onsoundstart後に発生するイベント。
onspeechstart: 音声認識のために使用される音声が開始されたときに発生。
onspeechend: 音声認識のために使用される音声が終了したときに発生。onspeechstart後に発生するイベント。
onerror: 音声認識サービスのエラーが発生したときに発生。SpeechRecognitionErrorのインターフェイスが返却される。
onresult: 音声認識サービスが結果を返すときに発生。SpeechRecognitionEventのインターフェイスが返却される。
onnomatch: 音声認識サービスは、信頼閾値を満たさないときに発生。

おわりに

これ、もっと精度が上がるか、精度を上げるような仕組みをちゃんと入れるか、テキスト解析APIに投げつけるかとか、色々応用することができそうです。
WebRTCと一緒に使ってウンヌン…とかもできそう。

以上!