compassが遅い件 vol.3
「compassが遅い件 vol.2」の続きです。今回はcssの生成を並列化したいと思います。
ディレクトリ構成等々は、vol.1・vol.2と同様です。
またもやconfig.rbに処理を書き込むカタチになります。
※ vol.1に記載した↓とは別件です。
> 最終的には、このグループでのコンパイルをforkさせ、複数のグループを並列にコンパイルさせるとこまでやりました。
> 今回は、それをするための前準備になります。
必要なものをインストールする
今回は並列実行ということで、Parallelというgemを使いたいと思います。
(スレッド数の管理もしてくれるし、とても楽に導入できたので。)
・Parallelをインストール(筆者はMacです…)
$ sudo gem install parallel
・Compassのバージョン
$ compass -v
Compass 0.12.2 (Alnilam)
Copyright (c) 2008-2014 Chris Eppstein
Released under the MIT License.
Compass is charityware.
Please make a tax deductable donation for a worthy cause: http://umdf.org/compass
並列化が実感できる環境をつくる
「vol.2」で作成したsass/test.scssはコンパイルに10秒以上かかっていたので、test2.scssとtest3.scssをつくり、css生成がブロックされる状態にしておく。
$ cp -ip sass/print.scss sass/test2.scss
$ cp -ip sass/print.scss sass/test3.scss
準備オッケー。
通常のコンパイルをしてみる。
$ compass compile --time --force
identical stylesheets/ie.css (0.0s)
identical stylesheets/print.css (0.0s)
identical stylesheets/screen.css (0.03s)
identical stylesheets/test.css (18.165s)
identical stylesheets/test2.css (0.001s)
identical stylesheets/test3.css (0.0s)
Compilation took 18.201s
test.css生成に時間がかかり、test2.cssとtest3.cssの生成がブロックされているのが分かります。
ソースの修正
修正箇所は、sass_filesのループで回してるところをparallelを使って並列化させるだけ。
ここの処理を_config.rbで上書きし、並列化させる。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'parallel' | |
module ::Compass | |
class Compiler | |
def run | |
failure_count = 0 | |
if new_config? | |
# Wipe out the cache and force compilation if the configuration has changed. | |
remove options[:cache_location] if options[:cache_location] | |
options[:force] = true | |
end | |
# Make sure the target directories exist | |
target_directories.each {|dir| directory dir} | |
# Compile each sass file. | |
result = timed do | |
Parallel.each(sass_files.zip(css_files), :in_threads => 2) do |sass_filename, css_filename| | |
begin | |
compile_if_required sass_filename, css_filename | |
rescue Sass::SyntaxError => e | |
failure_count += 1 | |
handle_exception(sass_filename, css_filename, e) | |
end | |
end | |
end | |
if options[:time] | |
puts "Compilation took #{(result.__duration * 1000).round / 1000.0}s" | |
end | |
return failure_count | |
end | |
end | |
end |
すると、上限2スレッドでcss生成が開始される。
(test.cssでブロック中にtest2.css、test3.cssが生成されてるのがわかる。
(test.cssでブロック中にtest2.css、test3.cssが生成されてるのがわかる。
$ compass compile --time --force
identical stylesheets/ie.css (0.001s)
identical stylesheets/print.css (0.001s)
identical stylesheets/screen.css (0.016s)
identical stylesheets/test2.css (0.001s)
identical stylesheets/test3.css (0.001s)
identical stylesheets/test.css (16.213s)
Compilation took 16.216s
オプションで指定できるようにする
例のごとく、環境変数でパラメータを受け取れるようにします。今回は、全ソース乗っけます。(いろいろ細かく書いていってたので、まとめの意味で。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### コンパイル対象を絞り込みするオプション | |
# 指定がない場合のコンパイル対象 | |
DEFAULT_TARGET_PATTERN = "" | |
# 値にマッチするscssファイルをコンパイル対象として絞り込む | |
SASS_FILE_PATTERNS = { | |
"IE" => "^.*ie.*$", | |
"PRINT" => "^.*print.*$", | |
} | |
TARGET_PATTERN = ENV['TARGET_PATTERN'] || DEFAULT_TARGET_PATTERN | |
# 画像生成のサイズをメモリにキャッシュさせる | |
DEFAULT_DIMENSIONS_CACHE = false | |
if ENV['DIMENSIONS_CACHE'] === "true" | |
DIMENSIONS_CACHE = true | |
elsif ENV['DIMENSIONS_CACHE'] === "false" | |
DIMENSIONS_CACHE = false | |
else | |
DIMENSIONS_CACHE = DEFAULT_DIMENSIONS_CACHE | |
end | |
# cssファイル生成のスレッド数 | |
MAX_COMPILE_THREADS = 2 #上限値(超えたら並列生成しない | |
DEFAULT_COMPILE_THREADS = 1 | |
COMPILE_THREADS = ENV['COMPILE_THREADS'] && ENV['COMPILE_THREADS'].to_i || DEFAULT_COMPILE_THREADS | |
### コンパイル対象を絞り込む対応 | |
module ::Compass | |
class Compiler | |
def sass_files(options = {}) | |
exclude_partials = options.fetch(:exclude_partials, true) | |
opt_sass_files = self.options[:sass_files] | |
# ファイル指定あり | |
if opt_sass_files | |
return opt_sass_files | |
end | |
# ファイル指定なし | |
sass_files = Dir.glob(separate("#{from}/**/#{'[^_]' if exclude_partials}*.s[ac]ss")) | |
##### 取得したsass_filesを操作することで、コンパイル対象を変更することができる | |
target_pattern = SASS_FILE_PATTERNS[TARGET_PATTERN] | |
# 指定がないので絞り込ままい | |
if target_pattern.nil? || target_pattern.empty? | |
return sass_files | |
end | |
# 指定があるので正規表現で絞り込み | |
target_regexp = Regexp.new(target_pattern) | |
_sass_files = [] | |
sass_files.each do |file| | |
_file = File.basename(file) | |
if target_regexp =~ _file | |
_sass_files.push(file) | |
end | |
end | |
return _sass_files | |
##### | |
end | |
end | |
end | |
### image-width/image-heightの高速化 | |
if DIMENSIONS_CACHE | |
module ::Compass::SassExtensions::Functions::ImageSize | |
@@dimensions_cached = {} | |
def image_dimensions(image_file) | |
if @@dimensions_cached[image_file.value].nil? | |
options[:compass] ||= {} | |
options[:compass][:image_dimensions] ||= {} | |
options[:compass][:image_dimensions][image_file.value] = ImageProperties.new(image_path_for_size(image_file.value)).size | |
@@dimensions_cached[image_file.value] = options[:compass][:image_dimensions][image_file.value] | |
end | |
@@dimensions_cached[image_file.value] | |
end | |
end | |
end | |
### cssファイル生成を並列化 | |
if MAX_COMPILE_THREADS >= COMPILE_THREADS && COMPILE_THREADS > 1 | |
require 'parallel' | |
module ::Compass | |
class Compiler | |
def run | |
failure_count = 0 | |
if new_config? | |
# Wipe out the cache and force compilation if the configuration has changed. | |
remove options[:cache_location] if options[:cache_location] | |
options[:force] = true | |
end | |
# Make sure the target directories exist | |
target_directories.each {|dir| directory dir} | |
# Compile each sass file. | |
result = timed do | |
Parallel.each(sass_files.zip(css_files), :in_threads => COMPILE_THREADS) do |sass_filename, css_filename| | |
begin | |
compile_if_required sass_filename, css_filename | |
rescue Sass::SyntaxError => e | |
failure_count += 1 | |
handle_exception(sass_filename, css_filename, e) | |
end | |
end | |
end | |
if options[:time] | |
puts "Compilation took #{(result.__duration * 1000).round / 1000.0}s" | |
end | |
return failure_count | |
end | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/ruby | |
command_options = ARGV | |
# config.rbに渡すパラメータを定義 | |
# 'key' => { // zoocompassで処理を分岐する場合に使うキー | |
# 'env' => '', // config.rbで使う環境変数名 | |
# 'opt' => '', // zoocompass実行時に指定するオプション名 | |
# } | |
# | |
custom_options = { | |
'compile_target' => { | |
'env' => 'TARGET_PATTERN', | |
'opt' => '--target', | |
}, | |
'dimensions_cache' => { | |
'env' => 'DIMENSIONS_CACHE', | |
'opt' => '--enable-dimensions', | |
}, | |
'compile_threads' => { | |
'env' => 'COMPILE_THREADS', | |
'opt' => '--compile-threads', | |
} | |
} | |
# custom_optionsのオプションを指定していた場合は、オプションから削除+環境変数へセットする | |
custom_options.each do |o,v| | |
index = command_options.index(v['opt']) | |
if !index.nil? | |
# 環境変数へセット | |
option = command_options.values_at(index + 1)[0] | |
ENV[v['env']] = option | |
# オプションから削除 | |
command_options.delete_at(index + 1) | |
command_options.delete_at(index) | |
end | |
end | |
# compassを実行する | |
compass_options = command_options.join(' ') | |
compass_command = 'compass ' + compass_options | |
system(compass_command) |
下記のようなコマンドで実行できます。
$ ./bin/zoocompass compile --time --force --compile-threads 2
終わりに
だいぶ複雑になってきましたね。。
苦肉の策でもあるので、ほんとにどうしようもないときに。。とか使いどきを選んでいただきたい所存。
以上!
苦肉の策でもあるので、ほんとにどうしようもないときに。。とか使いどきを選んでいただきたい所存。
以上!
Knockout.jsで大規模開発してみた件について。
筆者の担当していた案件では、Knockout.jsを使って開発していました。今回はどんな感じで使ってたのか?というのを記事にしたいと思います。
Knockout.jsとは?
MVVM(Model-View-ViewModel)パターンをサポートするフレームワークです。
双方向バインディング云々…書こうとしましたが、こちらを参照したほうが良いでしょう。
わかりやすいです。
[Knockout]MVVMパターンでアプリケーションを構築する
どんな案件?
- 内容
webviewを使ったスマートフォン向けのアプリ開発。
- サポート端末
Android4.0以上
iOS6.0以上
- 案件規模
フロントエンジニアは5名程度。
- コード量
愚直に↓をしてみると、数十万行
$ find . -name "*.js" | xargs wc -l
いいとこわるいとこ
いいとこ
- 学習コストが低い
機能がシンプル(このくらい)というのもあるが、チュートリアルがしっかりしているので、これだけやればすぐ案件に入れるレベルになる。
チュートリアル:http://learn.knockoutjs.com/
- 機能はシンプルだが、拡張しやすい
data-bind内で指定する、バインディングを独自に拡張することができます。
例えば、
スマホの場合、clickイベントとtouchイベントを両方使うと問題が起きやすいので、
筆者の案件ではclickバインディングではtouchstartイベントを使うようにカスタムバインディングで上書きしている。
作り方は、こんなに簡単です↓
- ko.utilsというユーティリティがついてくる
地味に便利です。
ko.utils.unwrapObservableなどはカスタムバインディングを作る際によく使います。
- 分業できる
開発チーム内で、下記のように分業することができる。
フロントチームのコーディング担当
HTML+CSSを使ってViewのみを作成。作成後、JavaScript担当へパス。
独自タグもないので、とっつきやすい。
フロントチームのJavaScript担当
サーバーチームとAPIの設計について相談し、ViewModelを作成。
そして、コーディング担当から受け取ったViewにdata-bind属性を追加し動きをつける。
サーバーチーム
JavaScript担当と相談しAPIを作成する。
わるいとこ
- テストしづらい
Karma(mocha+sinon-chai)を使ってテストをするようにしましたが、Viewと紐付いているViewModelの実装はテストしづらかったです。
View側のテストもできないし、何かいい手はないかなーという感じでした。
(案件内ではテスト必須にはならず)
- だめというか、開発者が気をつけてなきゃいけないとこ
View側に複雑なロジックを入れないようにすること。
<!-- if hogehoge().length < 10 || nantokahuragu -->
このぐらいだとまだ良いが、ネストが深くなったり条件が複雑になると、可読性がものすごく下がる。
withの多様も要注意。
どんな感じで使ってたか
基本は、このような感じ。
(実際はファイル結合してminファイルを出力したり、styleはcssにちゃんと書いてたり、いろいろやってます。あと、ディレクトリ構成も適当です。)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE HTML> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<title></title> | |
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/knockout/3.1.0/knockout-min.js"></script> | |
<script type="text/javascript" src="class.js"></script> | |
<script type="text/javascript" src="BaseVM.js"></script> | |
</head> | |
<body> | |
<div id="TestVM"> | |
<p data-bind="text: hoge, click: addHoge"></p> | |
<input type="text" data-bind="value: hoge, style: {width: '200px'}"> | |
</div> | |
<script type="text/javascript"> | |
var TestVM = BaseVM.extend({ | |
hoge: null, | |
init: function(callback) { | |
this._super(); | |
// 双方向バインディングする対象を定義 | |
this.hoge = ko.observable("hogehoge"); | |
callback && callback(); | |
}, | |
addHoge: function() { | |
this.hoge(this.hoge() + "hoge"); | |
} | |
}); | |
var testVM = new TestVM(function() { | |
console.log("callback!!"); | |
}); | |
ko.applyBindings(testVM, document.getElementById('TestVM')); | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* ViewModelのベースクラス | |
*/ | |
var BaseVM = Class.extend({ | |
init: function() { | |
console.log("base vm init!"); | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Class風の処理実装をさせるライブラリ | |
* | |
* @sample | |
* var Animal = Class.extend({ | |
* init: function() { | |
* console.log('animal'); | |
* }, | |
* walk: function() { | |
* console.log('walking'); | |
* } | |
* }); | |
* var Monkey = Animal.extend({ | |
* _super: 'super!', | |
* init: function () { | |
* this._super(); | |
* console.log('monkey'); | |
* } | |
* }); | |
* | |
* var monkey = new Monkey; | |
* monkey.walk(); | |
* => walking | |
*/ | |
(function (window) { | |
'use strict'; | |
/** | |
* メソッド内に_superの記述があるかのチェック | |
* _superがあるメソッドだけ上書き | |
* _superがあるかどうか判断できない場合は[/ .* /]となって全部上書き | |
*/ | |
var fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; | |
function Class() { /* noop. */ } | |
Class.extend = function (props) { | |
var SuperClass = this; | |
function Class() { | |
if (typeof this.init === 'function') { | |
this.init.apply(this, arguments); | |
} | |
// _superプロパティを書き込み禁止 | |
Object.defineProperty(this, '_super', { | |
value: undefined, | |
enumerable: false, | |
writable: false, | |
configurable: true | |
}); | |
} | |
// prototypeチェーンを作る | |
Class.prototype = Object.create(SuperClass.prototype, { | |
constructor: { | |
value: Class, | |
enumerable: false, | |
writable: true, | |
configurable: true | |
} | |
}); | |
// instanceメソッドをセット | |
Object.keys(props).forEach(function (key) { | |
var prop = props[key], | |
_super = SuperClass.prototype[key], | |
isMethodOverride = (typeof prop === 'function' && typeof _super === 'function' && fnTest.test(prop)); | |
if (isMethodOverride) { | |
Class.prototype[key] = function () { | |
var ret, | |
tmp = this._super; | |
// _superプロパティを設定 | |
Object.defineProperty(this, '_super', { | |
value: _super, | |
enumerable: false, | |
writable: false, | |
configurable: true | |
}); | |
ret = prop.apply(this, arguments); | |
// _superプロパティを書き込み禁止にする | |
Object.defineProperty(this, '_super', { | |
value: tmp, | |
enumerable: false, | |
writable: false, | |
configurable: true | |
}); | |
return ret; | |
}; | |
} else { | |
Class.prototype[key] = prop; | |
} | |
}); | |
Class.extend = SuperClass.extend; | |
return Class; | |
}; | |
window.Class = Class; | |
}(window)); |
javascriptでClass風の実装ができるようなライブラリを使い、継承して使用する。
※ 案件で使ってるコード・ライブラリではないです。
(バンバン継承しよう!というのは推奨せずでしたが。)
まあ、CoffeeScriptなりTypeScript使っとけよって感じかも知れませんが。
まあ、CoffeeScriptなりTypeScript使っとけよって感じかも知れませんが。
ある程度コードに規則性もできたので、保守しやすくなりました。
また、機能がシンプルというのもあり、
Knockout.jsのカスタムバインディングなどを使って、下記のような様々な仕組みも実装しました。
・1ページ内にViewModelが複数の場合の制御
・SEの制御
・ダイアログの制御
などなど
おわりに
フレームワーク選定の際に重視するのは、学習コストだったり運用のしやすさだと思うので、Knockout.jsは結構使いやすいんじゃないかなーと思いました。
また、使ってみてですが、筆者はKnockout.jsの不具合には遭遇しませんでした。
要素が多くなりすぎるとパフォーマンスに影響がでるということはありました。
(ko.applyBindings実行時に負荷がかかるが、Knockoutで処理させる箇所が数百箇所とかにならなければそんなに問題にならなかったです。)
小規模向け・プロトタイプで使うと良い。と書かれてる記事もよく見ますが、
小規模でも使いやすく、大規模でも使いやすいやつでした。
以上!
また、使ってみてですが、筆者はKnockout.jsの不具合には遭遇しませんでした。
要素が多くなりすぎるとパフォーマンスに影響がでるということはありました。
(ko.applyBindings実行時に負荷がかかるが、Knockoutで処理させる箇所が数百箇所とかにならなければそんなに問題にならなかったです。)
小規模向け・プロトタイプで使うと良い。と書かれてる記事もよく見ますが、
小規模でも使いやすく、大規模でも使いやすいやつでした。
以上!
WebRTCのRTCDataChannel APIを使って、複数人にデータを送信してみる
WebRTCで複数人接続してみるの続きです。今回は「RTCDataChannel API」を使って、データの送受信を試してみたいと思います。
複数人接続の記事の後なので、複数人に対してデータの送受信までを試してみます。
RTCDataChannel APIとは?
Peerの接続を利用して、データを送受信できる仕組み。・送受信できるデータ形式
Blob、ArrayBuffer、ArrayBufferViewのようなデータ形式をサポート。
・通信プロトコル
UDP、TCP、SCTPをサポート。
RTCDataChannel APIを使うには?
こんな感じで生成することができます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 端末A側のpeerを生成 | |
var peer = new webkitRTCPeerConnection({ | |
"iceServers": [{"url": "stun:stun.l.google.com:19302"}] | |
}); | |
// 端末A側のdataChannelを生成(dataChannelOptionは省略可能 | |
// オプションに関してはこちらの記事が詳しいです。 | |
// http://www.html5rocks.com/ja/tutorials/webrtc/datachannels/ | |
var dataChannelLabel = "test channel"; | |
var dataChannelOption = {}; | |
var dataChannel = peer.createDataChannel(dataChannelLabel, dataChannelOption); | |
dataChannel.onerror = function (error) { | |
// エラー時に動作 | |
console.log(error); | |
}; | |
dataChannel.onmessage = function (event) { | |
// Peer接続している端末から、send()された時に動作 | |
console.log(event.currentTarget.label); | |
console.log("data channel message:", event.data); | |
}; | |
dataChannel.onopen = function () { | |
// dataChannelの接続確立時に動作 | |
console.log("data channel opened"); | |
}; | |
dataChannel.onclose = function () { | |
// dataChannelの切断時に動作 | |
console.log("data channel closed"); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ondatachannelで、 | |
// 端末B側のdataChannel(event.channel)を取得できるので保持しておく | |
// createAnswerのコールバックのタイミングでもオッケイ。 | |
var remoteDataChannel = null; | |
peer.ondatachannel = function(event) { | |
remoteDataChannel = event.channel; | |
}; |
下記をconsoleから実行すると端末B側の「dataChannel.onmessage」が動作します。
remoteDataChannel.send("test test");
注意点ですが、Peerでの接続確立後に「peer.createDataChannel」をしても相手側の「peer.ondatachannel」は動作しませんでした。
オファー前に「peer.createDataChannel」をしとくと良いようです。
参考:http://blog.wnotes.net/blog/article/beginning-webrtc-datachannel
前回のソース(WebRTCで複数人接続してみる)を修正してみる
webrtc.jsを修正する・initの処理を変更(this.dataChannelsとthis.remoteDataChannelsを初期化させる処理追加
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 初期化 | |
* @param {Object} option { | |
* videoElementId: "", | |
* iceCandidateCallback: function() {}, | |
* dataChannels: [{ | |
* label: 'test channel', | |
* option: { | |
* ordered: false, // 順序を保証しない | |
* maxRetransmitTime: 3000, // ミリ秒 | |
* }, | |
* onmessageCallback: function() { | |
* console.log("test channel received!!"); | |
* } | |
* }] | |
* } | |
*/ | |
init: function(option) { | |
this.option = option; | |
this.peer = null; | |
this.localStream = null; | |
this.isStartVideo = false; | |
this.dataChannels = {}; | |
this.remoteDataChannels = {}; | |
}, |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* コネクションをはる | |
* @return {Boolean} true: RTCPeerConnectionが生成済み, false: 生成できなかった | |
*/ | |
connect: function() { | |
if (this.peer) { | |
return true; | |
} | |
if (!this.localStream) { | |
return false; | |
} | |
if (!this.option.videoElementId) { | |
return false; | |
} | |
this.peer = this.createPeer(); | |
if (this.option.dataChannels && this.option.dataChannels.length > 0) {// Data Channelの設定があれば接続させる | |
for (var i = 0, l = this.option.dataChannels.length; i < l; i++) { | |
var dataChannelOption = this.option.dataChannels[i]; | |
this.createDataChannel(dataChannelOption.label, dataChannelOption.option, dataChannelOption.onmessageCallback); | |
} | |
this.peer.ondatachannel = (function(event) { | |
this.remoteDataChannels[event.channel.label] = event.channel; | |
}.bind(this)); | |
} | |
this.peer.addStream(this.localStream); | |
var iceCandidateCallback = this.option.iceCandidateCallback; | |
this.peer.onicecandidate = function(event) { | |
iceCandidateCallback && iceCandidateCallback(event.candidate); | |
}; | |
var videoElementId = this.option.videoElementId; | |
this.peer.onaddstream = function(event) { | |
var video = document.getElementById(videoElementId); | |
video.src = window.webkitURL.createObjectURL(event.stream); | |
}; | |
return true; | |
}, |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Data Channelの生成 | |
* @param {String} label | |
* @param {Object} option | |
* @param {Function} onmessageCallback | |
*/ | |
createDataChannel: function(label, option, onmessageCallback) { | |
var dataChannel = this.peer.createDataChannel(label, option); | |
dataChannel.onerror = function (error) { | |
console.log(error); | |
}; | |
dataChannel.onmessage = function (event) { | |
console.log(event.currentTarget.label); | |
console.log("data channel message:", event.data); | |
onmessageCallback && onmessageCallback(); | |
}; | |
dataChannel.onopen = function () { | |
console.log("data channel opened"); | |
}; | |
dataChannel.onclose = function () { | |
console.log("data channel closed"); | |
}; | |
this.dataChannels[label] = dataChannel; | |
}, |
・createWebrtcInstanceの処理を変更(new するところに、dataChannelsを追加しただけ
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// WebRTCのインスタンスを生成する | |
// @param {String} id | |
// @return {Object} WebRTCインスタンス | |
createWebrtcInstance: function(id) { | |
var videoElementId = this.getVideoElementId(id); | |
var video = document.createElement('video'); | |
video.id = videoElementId; | |
video.autoplay = true; | |
document.getElementsByTagName('body')[0].appendChild(video); | |
return new Webrtc({ | |
videoElementId: videoElementId, | |
iceCandidateCallback: (function(candidate) { | |
this.sendMessage(this.messageTypes.candidate, {sendto: id, fromUserId: this.userId, candidate: candidate}); | |
}.bind(this)), | |
dataChannels: [{ | |
label: 'test channel', | |
option: { | |
ordered: false, // 順序を保証しない | |
maxRetransmitTime: 3000, // ミリ秒 | |
}, | |
onmessageCallback: function() { | |
console.log("test channel received!!"); | |
} | |
}] | |
}); | |
}, |
データ送受信の実行手順
1. WebRTCで複数人接続してみるの「作ってみた」の手順通り実施し、複数人接続をさせておく2. 全てのブラウザでDeveloper Toolsを開く
3. どれか1つでconsoleから下記を実行する
for (var i in webrtcTest.webRtcInstances) {4. 実行したタブ以外で、下記のようなメッセージがconsole上に流れていることを確認できる
if (i !== webrtcTest.localInstanceKey) {
webrtcTest.webRtcInstances[i].remoteDataChannels['test channel'].send("test dayo");
}
}
data channel message:test dayoできた!!
(個別に送信したい場合は、下記を実行する
webrtcTest.webRtcInstances[id].remoteDataChannels['test channel'].send("test dayo");※ idは送信したい端末のid
終わりに
ここまでやったらWebRTCはお腹いっぱい気味。つぎは、ファイルの送受信をできるようにするかなあ。。
(チャンクさせるようにするなんて面倒だ。。
まあ、そこまでやるならサービスレベルのものを作るときかなー。
あとは、peer.jsとかライブラリを使ってみるとか。
気が向いたらやります。
参考
・WebRTC data channelshttp://www.html5rocks.com/ja/tutorials/webrtc/datachannels/
・WebRTC-DataChannel使ってみたよ
http://blog.wnotes.net/blog/article/beginning-webrtc-datachannel
以上!
WebRTCで複数人接続してみる
WebRTCをさわってみる&手動シグナリングしてみるの続きです。今回は、WebRTCを使って複数人接続してみたいと思います。
1対1で接続するときとの違いは?
前回のおさらいですが、1対1で接続するとこんな感じになる。
![]() |
図1. 1on1の接続 |
No | 端末A | 端末B |
1 | video要素を作っておく | video要素を作っておく |
2 | Peerを生成 | Peerを生成 |
3 | メディアに接続(カメラの起動を許可すること) ・Peerにストリームを接続し、自身の端末の状態をvideo要素で閲覧できる状態になる | メディアに接続(カメラの起動を許可すること) ・Peerにストリームを接続し、自身の端末の状態をvideo要素で閲覧できる状態になる |
4 | 端末Bへ送信するOfferを生成 | |
5 | 端末Aで生成したOfferを受信 | |
6 | 端末Aへ送信するAnswerを生成 | |
7 | 端末Bで生成したAnswerを受信 | |
8 | 端末Bへ送信する経路情報を出力 | |
9 | 端末Aで生成した経路情報を受信 | |
10 | 端末Aへ送信する経路情報を出力 | |
11 | 端末Bで生成した経路情報を受信 |
じゃあ、複数人接続の場合はどうなるのか?
3端末のときの接続を例として記述します。
上記のように接続するには、
端末Aと端末Bと端末Cがそれぞれ、下記のようなPeerを生成しなければなりません。
・端末Aと端末BをつなぐPeerの生成、端末Aと端末CをつなぐPeerの生成
・端末Bと端末AをつなぐPeerの生成、端末Bと端末CをつなぐPeerの生成
・端末Cと端末AをつなぐPeerの生成、端末Cと端末BをつなぐPeerの生成
そして、4台目が追加になる場合は更に1つずつPeerを生成することになり、
それを動的に通知して接続処理をするようにしなければなりません。
結構複雑っす。
どういう流れで複数接続させるか?
3端末での接続手順を記載します。(灰色のセルは端末未接続時・25〜38、25`〜38`は並行で処理されます)
No | 端末A | 端末B | 端末C |
1 | 特定URLにアクセス | ||
2 | socket.ioでコネクションをはる | ||
3 | コネクションをはってるユーザー全員にコールする | ||
4 | いなかったので終わり。 | ||
5 | 特定URLにアクセス | ||
6 | socket.ioでコネクションをはる | ||
7 | コネクションをはってるユーザー全員にコールする | ||
8 | 端末Bからコールされた | ||
9 | 端末Aのsocketのidを端末Bに返却する | ||
10 | 端末Aのsocketのidを返却される | ||
11 | Peerを生成 | ||
12 | socketのidを元に、端末AへSDPを送信(オファー) | ||
13 | Peerを生成 | ||
14 | 端末BのSDPを受信(オファーをレシーブ) | ||
15 | socketのidを元に、端末BへSDPを送信(アンサー) | ||
16 | 端末AのSDPを受信(アンサーをレシーブ) | ||
17 | 端末Aへ端末Bまでの経路情報を送信 | ||
18 | 端末Bから経路情報を受信 | ||
19 | 端末Bへ端末Aまでの経路情報を送信 | ||
20 | 端末Aから経路情報を受信 | ||
21 | Peer to Peer! | Peer to Peer! | |
22 | 特定URLにアクセス | ||
23 | socket.ioでコネクションをはる | ||
24 | コネクションをはってるユーザー全員にコールする | ||
下記のAとC / BとCのやりとりは並行して行われる | |||
25 | 端末Cからコールされた | ||
26 | 端末Aのsocketのidを端末Cに返却する | ||
27 | 端末Aのsocketのidを返却される | ||
28 | Peerを生成 | ||
29 | socketのidを元に、端末AへSDPを送信(オファー) | ||
30 | Peerを生成 | ||
31 | 端末CのSDPを受信(オファーをレシーブ) | ||
32 | socketのidを元に、端末CへSDPを送信(アンサー) | ||
33 | 端末AのSDPを受信(アンサーをレシーブ) | ||
34 | 端末Aへ端末Cまでの経路情報を送信 | ||
35 | 端末Cから経路情報を受信 | ||
36 | 端末Cへ端末Aまでの経路情報を送信 | ||
37 | 端末Aから経路情報を受信 | ||
38 | Peer to Peer! | Peer to Peer! | |
25` | 端末Cからコールされた | ||
26` | 端末Bのsocketのidを端末Cに返却する | ||
27` | 端末Bのsocketのidを返却される | ||
28` | Peerを生成 | ||
29` | socketのidを元に、端末BへSDPを送信(オファー) | ||
30` | Peerを生成 | ||
31` | 端末CのSDPを受信(オファーをレシーブ) | ||
32` | socketのidを元に、端末CへSDPを送信(アンサー) | ||
33` | 端末CのSDPを受信(アンサーをレシーブ) | ||
34` | 端末Bへ端末Cまでの経路情報を送信 | ||
35` | 端末Cから経路情報を受信 | ||
36` | 端末Cへ端末Bまでの経路情報を送信 | ||
37` | 端末Bから経路情報を受信 | ||
38` | Peer to Peer! | Peer to Peer! |
基本的な流れは1対1の時と一緒ですが、
Peerの生成タイミングや、Peer生成後にローカルのストリームとの接続手順などが変わってきます。
(「コネクションはっているユーザー全員にコールする」のレスポンスでオファー送ることもできた。そうすると、通信回数としては最小になりそう。)
作ってみた
このソースを使うと、↓↓のような画面がでるので、手順にそって作業してみてください。・シグナリングサーバー(Node.js)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* WebRTCのシグナリングサーバー | |
*/ | |
var port = 8888; | |
var io = require('socket.io').listen(port); | |
var messageTypes = { | |
open: "open", | |
allCall: "allCall", | |
callAnswer: "callAnswer", | |
offer: "offer", | |
answer: "answer", | |
candidate: "candidate", | |
}; | |
io.sockets.on('connection', function(socket) { | |
var connectGroup = null; | |
// 接続時の処理 | |
socket.on('open', function(option) { | |
connectGroup = option.connectGroup | |
socket.join(option.connectGroup); | |
socket.emit('open', { | |
messageTypes: messageTypes, | |
userId: socket.id | |
}); | |
}); | |
// 切断時の処理 | |
socket.on('disconnect', function() { | |
console.log("disconnect"); | |
}); | |
// シグナリング処理 | |
socket.on('message', function(message) { | |
// 特定のユーザに送信 | |
var target = message.sendto || message.param.sendto; | |
if (target) { | |
socket.to(target).emit('message', message); | |
return; | |
} | |
// 接続グループ全員に送信 | |
socket.broadcast.to(connectGroup).emit('message', message); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "webrtc", | |
"version": "0.0.0", | |
"description": "", | |
"main": "app.js", | |
"scripts": { | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, | |
"author": "", | |
"license": "ISC", | |
"devDependencies": { | |
"socket.io": "^1.0.6" | |
} | |
} |
$ npm installそして、これで起動する。
$ node app.js
・Webサーバー(Class風実装は特に見ないでいいです。。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE HTML> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<title></title> | |
</head> | |
<body> | |
<input type="button" value="ここを押したら繋がるけどカメラを先に許可しといてね" onclick="webrtcTest.createConnection();"> | |
<style type="text/css"> | |
video { | |
width: 80px; | |
} | |
</style> | |
<script type="text/javascript"> | |
// Class風の実装をするライブラリ | |
(function (window) { | |
'use strict'; | |
/** | |
* メソッド内に_superの記述があるかのチェック | |
* _superがあるメソッドだけ上書き | |
* _superがあるかどうか判断できない場合は[/ .* /]となって全部上書き | |
*/ | |
var fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; | |
function Class() { /* noop. */ } | |
Class.extend = function (props) { | |
var SuperClass = this; | |
function Class() { | |
if (typeof this.init === 'function') { | |
this.init.apply(this, arguments); | |
} | |
// _superプロパティを書き込み禁止 | |
Object.defineProperty(this, '_super', { | |
value: undefined, | |
enumerable: false, | |
writable: false, | |
configurable: true | |
}); | |
} | |
// prototypeチェーンを作る | |
Class.prototype = Object.create(SuperClass.prototype, { | |
constructor: { | |
value: Class, | |
enumerable: false, | |
writable: true, | |
configurable: true | |
} | |
}); | |
// instanceメソッドをセット | |
Object.keys(props).forEach(function (key) { | |
var prop = props[key], | |
_super = SuperClass.prototype[key], | |
isMethodOverride = (typeof prop === 'function' && typeof _super === 'function' && fnTest.test(prop)); | |
if (isMethodOverride) { | |
Class.prototype[key] = function () { | |
var ret, | |
tmp = this._super; | |
// _superプロパティを設定 | |
Object.defineProperty(this, '_super', { | |
value: _super, | |
enumerable: false, | |
writable: false, | |
configurable: true | |
}); | |
ret = prop.apply(this, arguments); | |
// _superプロパティを書き込み禁止にする | |
Object.defineProperty(this, '_super', { | |
value: tmp, | |
enumerable: false, | |
writable: false, | |
configurable: true | |
}); | |
return ret; | |
}; | |
} else { | |
Class.prototype[key] = prop; | |
} | |
}); | |
Class.extend = SuperClass.extend; | |
return Class; | |
}; | |
window.Class = Class; | |
}(window)); | |
</script> | |
<script src="webrtc.js"> | |
/** | |
* webrtcに関するクラスを実装 | |
*/ | |
var Webrtc = Class.extend({ | |
/** | |
* 初期化 | |
* @param {Object} option { | |
* videoElementId: "", | |
* iceCandidateCallback: function() {} | |
* } | |
*/ | |
init: function(option) { | |
this.option = option; | |
this.peer = null; | |
this.localStream = null; | |
this.isStartVideo = false; | |
}, | |
/** | |
* コネクションをはる | |
* @return {Boolean} true: RTCPeerConnectionが生成済み, false: 生成できなかった | |
*/ | |
connect: function() { | |
if (this.peer) { | |
return true; | |
} | |
if (!this.localStream) { | |
return false; | |
} | |
if (!this.option.videoElementId) { | |
return false; | |
} | |
this.peer = this.createPeer(); | |
this.peer.addStream(this.localStream); | |
var iceCandidateCallback = this.option.iceCandidateCallback; | |
this.peer.onicecandidate = function(event) { | |
iceCandidateCallback && iceCandidateCallback(event.candidate); | |
}; | |
var videoElementId = this.option.videoElementId; | |
this.peer.onaddstream = function(event) { | |
var video = document.getElementById(videoElementId); | |
video.src = window.webkitURL.createObjectURL(event.stream); | |
}; | |
return true; | |
}, | |
/** | |
* Peerを生成 | |
* @return {Object} RTCPeerConnection | |
*/ | |
createPeer: function() { | |
return (new webkitRTCPeerConnection({ | |
"iceServers": [{"url": "stun:stun.l.google.com:19302"}] | |
})); | |
}, | |
/** | |
* videoにカメラを接続 | |
*/ | |
startVideo: function() { | |
var video; | |
if (this.option && this.option.videoElementId) { | |
video = document.getElementById(this.option.videoElementId); | |
} else { | |
console.log("no video!!"); | |
} | |
navigator.webkitGetUserMedia({video: true, audio: true}, | |
(function(stream) { | |
video.src = window.URL.createObjectURL(stream); | |
this.localStream = stream; | |
this.isStartVideo = true; | |
}.bind(this)), | |
(function(err) { | |
this.isStartVideo = false; | |
console.log(err); | |
}.bind(this)) | |
); | |
}, | |
/** | |
* SDPを生成する | |
* @param {String} sdpType: offer or answer | |
* @param {Function} callback | |
*/ | |
createSdp: function(sdpType, callback) { | |
var createSdpCallback = function(sdp) { | |
this.peer.setLocalDescription(sdp, | |
function() { | |
callback(sdp); | |
}, | |
function(err) { | |
console.log(err); | |
} | |
); | |
}.bind(this); | |
if (sdpType === "offer") { | |
this.peer.createOffer(createSdpCallback); | |
} else if (sdpType === "answer") { | |
this.peer.createAnswer(createSdpCallback); | |
} | |
}, | |
/** | |
* SDPを受け取る | |
* @param {Object} sdp | |
*/ | |
receiveSdp: function(sdp) { | |
var remoteSdp = new RTCSessionDescription(sdp); | |
this.peer.setRemoteDescription(remoteSdp, | |
function() { | |
if (remoteSdp.type === "offer") { | |
console.log("receive offer"); | |
} | |
if (remoteSdp.type === "answer") { | |
console.log("receive answer"); | |
} | |
}, | |
function(err) { | |
console.log(err); | |
} | |
); | |
}, | |
/** | |
* 経路情報を受け取る | |
* @param {Object} candidate | |
*/ | |
receiveCandidate: function(candidate) { | |
if (candidate) { | |
var candidate = new RTCIceCandidate(candidate); | |
this.peer.addIceCandidate(candidate); | |
} else { | |
console.log("finish candidate!!"); | |
} | |
} | |
}); | |
</script> | |
<script src="http://localhost:8888/socket.io/socket.io.js"></script> | |
<script type="text/javascript"> | |
var WebrtcTest = Class.extend({ | |
// 初期化 | |
init: function() { | |
this.socket = null; | |
this.userId = null;// 自身のsocket接続ID | |
this.messageTypes = {};// 通信するメッセージのタイプ | |
this.webRtcInstances = {};// WebRTCのインスタンスたち | |
this.localInstanceKey = 'local';// ローカルのWebRTCインスタンスのキー | |
this.socketReadyState = false;// socket接続ステータス | |
this.connectGroup = location.search;// socket接続グループ: 接続するグループは一旦クエリストリングで決める | |
this.connectUrl = 'http://localhost:8888/'; | |
}, | |
// コネクションをはる | |
createConnection: function() { | |
this.socket = io.connect(this.connectUrl); | |
this.socket.on('connect', this.onConnect.bind(this)); | |
this.socket.on('open', this.onOpen.bind(this)); | |
this.socket.on('message', this.onMessage.bind(this)); | |
}, | |
// コネクション確立時の処理 | |
// @param {Object} event | |
onConnect: function(event) { | |
if (!this.socketReadyState) { | |
this.socket.emit('open', {connectGroup: this.connectGroup});// 接続成功時、接続先のグループを通知する | |
} | |
this.socketReadyState = true; | |
}, | |
// コネクション確立後の処理 | |
// @param {Object} event | |
onOpen: function(event) { | |
this.messageTypes = event.messageTypes; | |
this.userId = event.userId; | |
this.sendMessage(this.messageTypes.allCall, {fromUserId: this.userId});// 接続グループ全員に、新たな接続者がいることを通知する | |
}, | |
// ローカルのWebRTCのインスタンスを取得する | |
// @return {Object} WebRTCインスタンス | |
getLocalInstance: function() { | |
return this.getWebrtcInstance(this.localInstanceKey); | |
}, | |
// WebRTCのインスタンスを取得する | |
// @param {String} id | |
// @return {Object} WebRTCインスタンス | |
getWebrtcInstance: function(id) { | |
if (!this.webRtcInstances[id]) { | |
this.webRtcInstances[id] = this.createWebrtcInstance(id); | |
if (id !== this.localInstanceKey) {// 共有するためにローカルのストリームを接続する | |
this.webRtcInstances[id].localStream = this.webRtcInstances[this.localInstanceKey].localStream; | |
} | |
} | |
return this.webRtcInstances[id]; | |
}, | |
// WebRTCのインスタンスを生成する | |
// @param {String} id | |
// @return {Object} WebRTCインスタンス | |
createWebrtcInstance: function(id) { | |
var videoElementId = this.getVideoElementId(id); | |
var video = document.createElement('video'); | |
video.id = videoElementId; | |
video.autoplay = true; | |
document.getElementsByTagName('body')[0].appendChild(video); | |
return new Webrtc({ | |
videoElementId: videoElementId, | |
iceCandidateCallback: (function(candidate) { | |
this.sendMessage(this.messageTypes.candidate, {sendto: id, fromUserId: this.userId, candidate: candidate}); | |
}.bind(this)) | |
}); | |
}, | |
// video要素のIDを取得する | |
// @param {String} id | |
// @return {String} video要素のID | |
getVideoElementId: function(id) { | |
return 'video-' + id; | |
}, | |
// メッセージ受信処理 | |
// @param {Object} event | |
onMessage: function(event) { | |
console.log(event); | |
// 他のユーザーがログインしたときの応答処理 | |
if (this.messageTypes.allCall === event.param.type) { | |
console.log("group all call"); | |
var fromUserId = event.param.fromUserId; | |
this.sendMessage(this.messageTypes.callAnswer, {sendto: fromUserId, fromUserId: this.userId});// 応答する | |
} | |
// ログイン時に他ユーザー全員にコールしたときの応答があったときの処理 | |
if (this.messageTypes.callAnswer === event.param.type) { | |
console.log("call answer"); | |
var fromUserId = event.param.fromUserId; | |
var webrtc = this.getWebrtcInstance(fromUserId); | |
if (!webrtc.connect()) { | |
return false;// videoをオンにしてないときとか | |
} | |
webrtc.createSdp("offer", function(sdp) { | |
this.sendMessage(this.messageTypes.offer, {sendto: fromUserId, fromUserId: this.userId, sdp: sdp});// 応答があった人へオファー | |
}.bind(this)); | |
} | |
// オファーを受け取って、アンサーを返す | |
if (this.messageTypes.offer === event.param.type) { | |
console.log("offer"); | |
var fromUserId = event.param.fromUserId; | |
var sdp = event.param.sdp; | |
var webrtc = this.getWebrtcInstance(fromUserId); | |
if (!webrtc.connect()) { | |
return false;// videoをオンにしてないときとか | |
} | |
webrtc.receiveSdp(sdp); | |
webrtc.createSdp("answer", function(sdp) { | |
this.sendMessage(this.messageTypes.answer, {sendto: fromUserId, fromUserId: this.userId, sdp: sdp});// オファーがきたのでアンサー | |
}.bind(this)); | |
} | |
// アンサーを受け取る | |
if (this.messageTypes.answer === event.param.type) { | |
console.log("answer"); | |
var fromUserId = event.param.fromUserId; | |
var webrtc = this.getWebrtcInstance(fromUserId); | |
var sdp = event.param.sdp; | |
webrtc.receiveSdp(sdp); | |
} | |
// 経路情報を受け取る | |
if (this.messageTypes.candidate === event.param.type) { | |
console.log("receive candidate"); | |
var fromUserId = event.param.fromUserId; | |
var webrtc = this.getWebrtcInstance(fromUserId); | |
webrtc.receiveCandidate(event.param.candidate);// 飛んできた経路情報を取得 | |
} | |
}, | |
// メッセージを通知する | |
// @param {String} messageType | |
// @param {Object} param | |
// @param {Function} callback | |
sendMessage: function(messageType, param, callback) { | |
param.type = messageType; | |
this.socket.emit('message', {messageType: messageType, param: param, callback: callback}); | |
} | |
}); | |
var webrtcTest = new WebrtcTest(); | |
var localVideo = webrtcTest.getLocalInstance(); | |
localVideo.startVideo(); | |
</script> | |
</body> | |
</html> |
上記のソースをDocumentRoot以下に配置し、ブラウザからアクセスして、動作確認をしてみる。
1. 特定のURLへアクセス(筆者はここhttp://localhost/index.html
![]() |
図3. カメラ・マイクへのアクセス確認 |
2. カメラの表示確認
![]() |
図4. カメラ動画表示 |
そして、「ここを押したら繋がるけどカメラを先に許可しといてね」ボタンを押すことで、シグナリングサーバーへアクセスします。
(自分がページを開いてるよ。ってことを通知してるだけで、ここの手順では何も起こりません。)
※上記手順の1〜4です。
3. リモートのカメラ画像を表示する
![]() |
図5. リモートのカメラ動画表示 |
終わりに。
タブを複数開いて通知確認しながらテストしてたのですが、頭がこんがらがるし、結構たいへんでした。シグナリングサーバーを応用! 「WebRTCを使って複数人で話してみよう」を参考にさせて頂きました!
つぎこそはDataChannelやるぞー!
以上!
Web ComponentsとPolymerをさわってみる。
いまさらですが、Web Componentsってナニ?
ってことをサラッと書きたいと思います。
下記のW3C 提案をまとめてWeb Components というらしい。
Custom Elements
⇒ ウェブ開発者が HTML に独自のタグを追加できる仕組み。
HTML Imports
⇒ HTML Imports は別ファイルに定義した Custom Elements をページ内に読み込む仕組み。
(プログラミング言語の require や import みたいなもの)
HTML Templates
⇒ ページ本体のDOMと切り離されたHTMLを生成することができる仕組み。
(HTML Templates に定義された <template> タグでテンプレート用の HTML を囲んでおくと、 ページ本体の DOM とは切り離され、<img> や <video> を書いてもロードはおこらないし、<style> は適用されず <script> は実行されない)
Shadow DOM
⇒ DOM要素のレンダリング結果を、その要素が持つDOM のサブツリーとは独立に与える仕組み。
(DOMツリーをカプセル化し、ページの他の部分からDOMツリーを分離することができる)
Web Animations
⇒ CSS Animations と CSS Transitions と SVG Animations を 統合する仕組み。
Pointer Events
⇒ マウス、タッチ、ペンのイベントを統一する仕組み。
そこで、Polymerの提供しているplatform.jsを使えば該当APIをJavaScriptで再実装し、ブラウザにまだない機能を補ってくれる。
Web ComponentsのPolyfillライブラリがplatform.jsで、その上で動作するUIフレームワークがpolymer.js。
Polymerにはdesigner toolもあり、コードを書かなくてもUIを生成することができます。
(使ったことないけど、いい感じなのかな?)
更に、Polymerにはレイアウトの例だったり、アイコン群のサンプルがあります。
PolymerとWeb Componentsの関連は、下記を見ると分かりやすいです。
(各APIの土台の上に、platform.jsがあり、polymer.jsがある)
*画像をお借りしました!
(/test/webcomponents/index.htmlにおく
ブラウザで下記URLにアクセスすると、こんな表示になるとはず。(線と右上のは気にしないでください…
http://localhost/test/webcomponents/index.html
ちなみに、polymer使用時のネットワークタブはこんな感じになります。
ソース見てて思ったんですが、結構直列で取得しにいってますね。
この順番で取得しにいってます。
1. test/webcomponents/index.html
2. bower_components/core-toolbar/core-toolbar.html
3a. bower_components/core-toolbar/core-toolbar.css
3b. bower_components/polymer/polymer.html
4a. bower_components/polymer/layout.html
4b. bower_components/polymer/polymer.js
この仕組み、結構遅いんじゃないの…
ということで、インラインにしてくれる仕組みなどが既にあるようです。
まあ、これだけじゃよく分からないと思うので、次回以降に詳細を書ければいいなーと思います。
Polymerウンヌンから入るより、Web Componentsの各APIの詳細を掘り下げていったほうが分かりやすい気がする。
ってことをサラッと書きたいと思います。
Web Componentsとは
Web Componentsは新しく提案されたウェブブラウザ向け API 一式の総称。下記のW3C 提案をまとめてWeb Components というらしい。
Custom Elements
⇒ ウェブ開発者が HTML に独自のタグを追加できる仕組み。
HTML Imports
⇒ HTML Imports は別ファイルに定義した Custom Elements をページ内に読み込む仕組み。
(プログラミング言語の require や import みたいなもの)
HTML Templates
⇒ ページ本体のDOMと切り離されたHTMLを生成することができる仕組み。
(HTML Templates に定義された <template> タグでテンプレート用の HTML を囲んでおくと、 ページ本体の DOM とは切り離され、<img> や <video> を書いてもロードはおこらないし、<style> は適用されず <script> は実行されない)
Shadow DOM
⇒ DOM要素のレンダリング結果を、その要素が持つDOM のサブツリーとは独立に与える仕組み。
(DOMツリーをカプセル化し、ページの他の部分からDOMツリーを分離することができる)
Web Animations
⇒ CSS Animations と CSS Transitions と SVG Animations を 統合する仕組み。
Pointer Events
⇒ マウス、タッチ、ペンのイベントを統一する仕組み。
Polymerとは?
新しいAPI に依存してしまい、一体どのブラウザなら動くのかということになる。そこで、Polymerの提供しているplatform.jsを使えば該当APIをJavaScriptで再実装し、ブラウザにまだない機能を補ってくれる。
Web ComponentsのPolyfillライブラリがplatform.jsで、その上で動作するUIフレームワークがpolymer.js。
Polymerにはdesigner toolもあり、コードを書かなくてもUIを生成することができます。
(使ったことないけど、いい感じなのかな?)
更に、Polymerにはレイアウトの例だったり、アイコン群のサンプルがあります。
PolymerとWeb Componentsの関連は、下記を見ると分かりやすいです。
(各APIの土台の上に、platform.jsがあり、polymer.jsがある)
![]() |
図. PolymerとWeb Components |
Polymerを試してみる
・インストールする
(筆者はDocumentRoot以下の、/test/webcomponentsで実行
(筆者はDocumentRoot以下の、/test/webcomponentsで実行
# Bower のインストール・実行用のソースを用意する
$ sudo npm install bower -g
# 初期化処理
$ bower init
# Polymer のインストール
$ bower install --save Polymer/polymer
# コンポーネントのインストール
$ bower install --save Polymer/core-elements
$ bower install --save Polymer/paper-elements
(/test/webcomponents/index.htmlにおく
<!DOCTYPE HTML>・アクセスする
<html lang="ja">
<head>
<meta charset="UTF-8">
<title></title>
<script src="bower_components/platform/platform.js"></script>
<link rel="import" href="bower_components/core-toolbar/core-toolbar.html">
</head>
<body>
<core-toolbar>TEST</core-toolbar>
</body>
</html>
ブラウザで下記URLにアクセスすると、こんな表示になるとはず。(線と右上のは気にしないでください…
http://localhost/test/webcomponents/index.html
![]() |
図. core-toolbarをつかう |
ちなみに、polymer使用時のネットワークタブはこんな感じになります。
![]() |
図. polymer使用時のネットワークタブ |
この順番で取得しにいってます。
1. test/webcomponents/index.html
2. bower_components/core-toolbar/core-toolbar.html
3a. bower_components/core-toolbar/core-toolbar.css
3b. bower_components/polymer/polymer.html
4a. bower_components/polymer/layout.html
4b. bower_components/polymer/polymer.js
この仕組み、結構遅いんじゃないの…
ということで、インラインにしてくれる仕組みなどが既にあるようです。
まあ、これだけじゃよく分からないと思うので、次回以降に詳細を書ければいいなーと思います。
Polymerウンヌンから入るより、Web Componentsの各APIの詳細を掘り下げていったほうが分かりやすい気がする。
終わりに
この技術が標準化されてきたら、Web業界の職種も変わってきそうですね。
web componentsストアみたいなのできないかなー
参考にさせて頂きました!
Web Component概要
web componentsストアみたいなのできないかなー
参考にさせて頂きました!
Web Component概要
Polymer と Web Components
以上!
WebRTCをさわってみる&手動シグナリングしてみる
今回は、WebRTCについて書きたいと思います。WebRTCって何ぞ?
WebRTCって何ぞ?ってとこから書きたいなと思ったのですが、他の方の記事見てるとよく書けてるな。。
改めて書く必要ないんじゃないかな。。
図ないと分かりづらいよな。。面倒だなこれ。。
って感じになりました。
ということで、WebRTCって何ぞ?とかは下記の記事を参考にしてください!
・WebRTCを仕組みから実装までやってみる
http://blog.wnotes.net/blog/article/webrtc-beginning
・WebRTCで変わるWebの未来
http://www.qcontokyo.com/data_2013/ToruYoshikawa_QConTokyo2013.pdf
んで、今回書くのは
P2Pで通信を開始するまでのシーケンスを1手順ずつ、自分でわかるように、自分のために書きます。
今回やろうとすること
端末A・端末BをPeer to Peerし、Macに内蔵しているカメラで撮影している内容をリアルタイムに共有するところまでやる。
必要っぽいもの
シグナリングサーバーICEサーバー(STUN / TURN)
と思ったけど、シグナリングは手動でできそうだし、ICEサーバーだけでいいんじゃないか感。
ということで、今回は↓でやります。
・シグナリングは手動
・Googleが提供しているSTUNサーバー
接続手順
事前準備(共通)No | 端末A | 端末B |
1 | video要素を作っておく | video要素を作っておく |
2 | Peerを生成 | Peerを生成 |
3 | メディアに接続(カメラの起動を許可すること) ・Peerにストリームを接続し、自身の端末の状態をvideo要素で閲覧できる状態になる | メディアに接続(カメラの起動を許可すること) ・Peerにストリームを接続し、自身の端末の状態をvideo要素で閲覧できる状態になる |
この作業は接続する端末共通です。
ここまで実施することで、ブラウザで自身の姿が見れるようになるはずです。
接続対象へSDPを送受信してセッションを確立
Simple Traversal of UDP through NATs (STUN): NAT越えの方法としてRFC3489で定められた標準的な仕組み。
外部のSTUNサーバに対してクライアントが一度接続し、グローバルIPとマッピングされたポート番号を記憶しておくことで、そのデータを使ってPeerは相手のマシンを特定することができる。
No | 端末A | 端末B |
4 | 端末Bへ送信するOfferを生成 | |
5 | 端末Aで生成したOfferを受信 | |
6 | 端末Bへ送信するAnswerを生成 | |
7 | 端末Bで生成したAnswerを受信 |
リモート側のストリームを共有する
接続経由を共有することで、端末Aと端末BをPeer to Peer接続する。
No | 端末A | 端末B |
8 | 端末Bへ送信する経路情報を出力 | |
9 | 端末Aで生成した経路情報を受信 | |
10 | 端末Aへ送信する経路情報を出力 | |
11 | 端末Bで生成した経路情報を受信 |
ここまでくると、接続した端末のカメラの動画が表示されるようになっているはずです。
まとめて書くと、こんな手順です。
No | 端末A | 端末B |
1 | video要素を作っておく | video要素を作っておく |
2 | Peerを生成 | Peerを生成 |
3 | メディアに接続(カメラの起動を許可すること) ・Peerにストリームを接続し、自身の端末の状態をvideo要素で閲覧できる状態になる | メディアに接続(カメラの起動を許可すること) ・Peerにストリームを接続し、自身の端末の状態をvideo要素で閲覧できる状態になる |
4 | 端末Bへ送信するOfferを生成 | |
5 | 端末Aで生成したOfferを受信 | |
6 | 端末Aへ送信するAnswerを生成 | |
7 | 端末Bで生成したAnswerを受信 | |
8 | 端末Bへ送信する経路情報を出力 | |
9 | 端末Aで生成した経路情報を受信 | |
10 | 端末Aへ送信する経路情報を出力 | |
11 | 端末Bで生成した経路情報を受信 |
手動でやると面倒ですね。これ。
接続してみよう!
コードはこんな感じ(1〜11の上記手順通り番号をつけています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE HTML> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<title></title> | |
</head> | |
<body> | |
<style type="text/css"> | |
video { | |
width: 480px; | |
height: 360px; | |
margin: 5px; | |
padding: 0; | |
border: 10px solid #000; | |
} | |
textarea { | |
width: 480px; | |
height: 200px; | |
margin: 5px; | |
padding: 0; | |
border: 10px solid #0f0; | |
} | |
</style> | |
<script type="text/javascript"> | |
var webrtc = {}; | |
(function(webrtc) { | |
// 1. video要素が必要 | |
var body = document.getElementsByTagName('body')[0]; | |
var localVideo = document.createElement('video'); | |
localVideo.autoplay = true; | |
var remoteVideo = document.createElement('video'); | |
remoteVideo.autoplay = true; | |
var receiveSdpInput = document.createElement('textarea'); | |
receiveSdpInput.value = "ここにはSDPを貼り付けてね"; | |
var candidatesInput = document.createElement('textarea'); | |
candidatesInput.value = "ここにはCandidateを貼り付けてね"; | |
body.appendChild(localVideo); | |
body.appendChild(remoteVideo); | |
body.appendChild(receiveSdpInput); | |
body.appendChild(candidatesInput); | |
// 2. Peerを生成 | |
// Googleが提供しているSTUNサーバを使う | |
// Simple Traversal of UDP through NATs (STUN): NAT越えの方法としてRFC3489で定められた標準的な仕組み | |
// 外部のSTUNサーバに対してクライアントが一度接続し、グローバルIPとマッピングされたポート番号を記憶しておくことで、そのデータを使ってPeerは相手のマシンを特定することができる | |
var peer = new webkitRTCPeerConnection({ | |
"iceServers": [{"url": "stun:stun.l.google.com:19302"}] | |
}); | |
// 3. メディアに接続(カメラ | |
// メディアに接続しvideo要素に内容を表示する | |
navigator.webkitGetUserMedia({video: true, audio: false}, | |
function(stream) { | |
// blob URLを指定するとカメラの画像video要素で閲覧できる | |
// 自分の顔を見たくなかったらコメントアウトしてね | |
localVideo.src = window.URL.createObjectURL(stream); | |
// Peerにストリームを接続 | |
peer.addStream(stream); | |
}, | |
function(err) { | |
console.log(err); | |
} | |
); | |
// --- 接続対象へSDPを送受信し、セッションを確立させる | |
// Session Description Protocol (SDP): 各ブラウザの情報(セッションが含むメディアの種類、IPアドレス、ポート番号などなど)を示し、文字列で表現される | |
// 本来ならばシグナリングサーバーを用意して接続対象を選びSDPを送信するという手順になるが、用意が面倒なので手動でやる | |
// 4. Offer生成: 端末A | |
// 接続対象へSDPを送信する(今回はconsole.logに出力するだけ | |
var createOffer = function() { | |
peer.createOffer(function(sdp) { | |
peer.setLocalDescription(sdp, | |
function() { | |
// 本来ならばこのタイミングでシグナリングサーバーへ自分のSDPを送信する | |
console.log(sdp); | |
}, | |
function(err) { | |
console.log(err); | |
} | |
); | |
// コピーしやすいように文字列化 | |
console.log(JSON.stringify(sdp)); | |
}); | |
}; | |
webrtc.createOffer = createOffer; | |
// 5. Offerを受信: 端末B | |
// SDPを受信する(手順4. で生成したSDPをコピーしテキストエリアに入力する. その後、Developer Toolsのコンソールから下記関数を実行する | |
var receiveSdp = function() { | |
var sdp = receiveSdpInput.value;// コンソールから値を渡すようにしたかったが、自動文字変換が走ってうまくいかないので左側のテキストエリアに記載して値を取る | |
sdp = JSON.parse(sdp); | |
var remoteSdp = new RTCSessionDescription(sdp); | |
peer.setRemoteDescription(remoteSdp, | |
function() { | |
if (remoteSdp.type === "offer") { | |
console.log("receive offer"); | |
} | |
if (remoteSdp.type === "answer") { | |
console.log("receive answer"); | |
} | |
}, | |
function(err) { | |
console.log(err); | |
} | |
); | |
}; | |
webrtc.receiveSdp = receiveSdp; | |
// 6. Answer生成: 端末B | |
// 接続対象へSDPを送信する(今回はconsole.logに出力するだけ | |
var createAnswer = function() { | |
peer.createAnswer(function(sdp) { | |
peer.setLocalDescription(sdp, | |
function() { | |
console.log(sdp); | |
}, | |
function(err) { | |
console.log(err); | |
} | |
); | |
// コピーしやすいように文字列化 | |
console.log(JSON.stringify(sdp)); | |
}); | |
}; | |
webrtc.createAnswer = createAnswer; | |
// 7. Answerを受信: 端末A | |
// SDPを受信する(手順6. で生成したSDPをコピーしテキストエリアに入力する. その後、Developer Toolsのコンソールから5で定義した下記関数を実行する | |
// webrtc.receiveSdp() | |
// --- ストリームの共有 | |
peer.onaddstream = function(event) { | |
remoteVideo.src = window.webkitURL.createObjectURL(event.stream); | |
}; | |
// 通常は1経路ずつやりとりするが、手動では辛いので配列に入れていっぺんに共有する | |
var candidates = []; | |
peer.onicecandidate = function(event) { | |
candidates.push(event.candidate); | |
}; | |
// 8. candidateの出力: 端末A | |
var displayCandidates = function() { | |
// コピーしやすいように文字列化 | |
console.log(JSON.stringify(candidates)); | |
}; | |
webrtc.displayCandidates = displayCandidates; | |
// 9. candidateの受信: 端末B | |
var receiveCandidates = function() { | |
var candidates = candidatesInput.value;// コンソールから値を渡すようにしたかったが、自動文字変換が走ってうまくいかないので右側のテキストエリアに記載して値を取る | |
candidates = JSON.parse(candidates); | |
for (var i = 0, l = candidates.length; i < l; i++) { | |
if (candidates[i]) { | |
var candidate = new RTCIceCandidate(candidates[i]); | |
peer.addIceCandidate(candidate); | |
} else { | |
console.log("no candidate"); | |
} | |
} | |
}; | |
webrtc.receiveCandidates = receiveCandidates; | |
// 10. candidateの出力: 端末B | |
// 8.と同様の手順 | |
// 11. candidateの受信: 端末A | |
// 9.と同様の手順 | |
})(webrtc); | |
</script> | |
</body> | |
</html> |
![]() |
図. webrtcテストサンプル |
(カメラを許可すると、左上に端末A(自分の顔)が表示されます。
では、ブラウザを2つ立ち上げ(別のPCでも可)、端末Aと端末Bとして作業してみましょう。
4〜11は下記のような作業が必要なので実行してみてください。
![]() |
図. 手順説明用 |
No | 端末A | 端末B |
4 | 端末Bへ送信するOfferを生成 ・Developer ToolsのConsoleで下記を実行し、表示される文字列をコピーする(文字列に変換して表示されてるやつ webrtc.createOffer() | |
5 | 端末Aで生成したOfferを受信 ・4でコピーした文字列をブラウザに表示されてるテキストエリア左側(図の①)に貼り付け、Consoleから下記を実行する webrtc.receiveSdp() | |
6 | 端末Aへ送信するAnswerを生成 ・Consoleで下記を実行し、表示される文字列をコピーする(文字列に変換して表示されてるやつ webrtc.createAnswer() | |
7 | 端末Bで生成したAnswerを受信 ・6でコピーした文字列をブラウザに表示されてるテキストエリア左側(図の①)に貼り付け、Consoleから下記を実行する webrtc.receiveSdp() | |
8 | 端末Bへ送信する経路情報を出力 ・Consoleで下記を実行し、表示される文字列をコピーする(文字列に変換して表示されてるやつ webrtc.displayCandidates() | |
9 | 端末Aで生成した経路情報を受信 ・8でコピーした文字列をブラウザに表示されてるテキストエリア右側(図の②)に貼り付け、Consoleから下記を実行する webrtc.receiveCandidates() | |
10 | 端末Aへ送信する経路情報を出力 ・Consoleで下記を実行し、表示される文字列をコピーする(文字列に変換して表示されてるやつ webrtc.displayCandidates() | |
11 | 端末Bで生成した経路情報を受信 ・10でコピーした文字列をブラウザに表示されてるテキストエリア右側(図の②)に貼り付け、Consoleから下記を実行する webrtc.receiveCandidates() |
11を実行することで、右上に端末B(相手の顔)が表示されます。
おわりに
手動でやるとことで、流れがだいぶ理解できました。
今度は自動でシグナリングするようにして、DataChannelでもいじってみたいと思います。
以上!
Android4.4 WebviewでGoogle NotoSansを使ってハマったポイント
筆者がAndroid4.4のWebviewでNoto Sansを使った際にハマった点を忘れないように書いておきます。Noto Sansってなに?
Noto SansとはGoogleとAdobeが共同で開発したフリーフォントです。
AdobeからはSource HanSansという名称でリリースされています。
![]() |
Noto Sans Japanese |
Noto Sansは無償で使えるうえ、改編・再配布も可能です。
日本語にも対応しており漢字も多く扱っています。
ウェイトも7種類用意されておりクオリティーも高い。
つまり、とても素敵なフォントだと言うことです。
Noto Sans Japanese
ハマりポイント
で、何にハマったかと言うと、、
Android4.4だけ文字が上にズレてしまうんです(泣)
![]() |
Android4.4以外 |
![]() |
Android4.4 |
調べてみるとこんなことが、
`Android Chromeでline-heightがかわる?`
どうやら、Android4.4ではベースラインが少し上に上がっているようです。
リンク先のブログさんと同じく、
User agentがAndroid4.4であればline-heightを調整するよう対応しました。
通常のNoto Sansでは発生しなかったので、日本語が入っているとベースラインが狂うのかもしれないです。このへん詳しくはわかっていません。
CSS3アニメーションでハマったこと。
筆者が以前担当していたWebviewを使ったスマホアプリの案件で、CSS3でFlashバリのアニメーションを作る。ということをやったので、その時にハマったことを書いていきたいと思います。
既存のFlashアニメーションをスマホでも表現したいと言われたのが背景です。
ツールとかは使わず、独自に書いていくスタイルでした。(1人でやってた。
どんな案件?
下記をサポートしたWebviewアプリ。Android2.3系
Android4.x系
iosのいくつか忘れた
ハマったポイント
再現機種 | 現象 | 原因 / 対策 |
Gylaxy S XperiaAcroHD (Android2.3系) | border-radiusを指定しても、角丸が再現できない。 | 特定のAndroid端末では、border-radiusで%(パーセント)指定ができない。 bordera-radiusをpx指定にする。 |
Android2.3系 | animation-fill-modeプロパティを指定していても、アニメーション後の表示を維持することができない。(forwards) | サポートしていないプロパティだった。 筆者は、予めアニメーション終了後のプロパティを指定しておくことで回避した。 |
Android2.3系 | -webkit-filterを指定しても、画像にエフェクトがかからない。 | サポートしていないプロパティだった。 エフェクトのかかった画像にするなどして、使わないようにする。 |
Gylaxy S3 一部Android? | アニメーション中、画像がちらつく。 | -webkit-backface-visibility:hiddenを指定すると収まる。 要素全体を囲むと別の要因を引き起こすこともあるので、使うときは要注意。 |
Gylaxy S XperiaAcroHD (Android2.3系) | -webkit-transform:scale で要素が拡大できない。 | -webkit-backface-visibility:hiddenを指定していることが原因だった。 拡大したい要素には指定しないようにする。 |
Android2.3系 | 一気に複数のアニメーションをスタートさせるとカクつく・アニメーションが変な動きをする | そもそも、一気に複数のアニメーションをスタートさせないように作る。 どうしてもっていう場合は、keyframeの0%〜50%を動作させない・0.5秒後からアニメーション実行させるなど遅延実行にすると大丈夫だった。 |
iOS6? | z-indexが効かない。 | 親要素でもz-indexを指定してあげることで効くようになった。 |
Android2.3系 | overflow:scrollが効かない。 | CSSでスクロールできないのでJSを使ってスクロールさせる必要がある。 |
Android2.3系 | モーダル時、下部の要素のイベントが発火してしまう。 | 上の要素のタップイベントを通り越して下の要素のタップイベントが発火する。 jsで、e.preventDefault()でイベントをキャンセルさせる。 |
Android4.0x系 | jsでクラスを置き換えても、表示が切り替わらないことがある。 | 再レンダリングが走らない? 原因分からず。 |
Android2.3系 | アニメーションを0%、100% のみで指定すると、アニメーション中に他要素のtransformが効かなくなることがある。 | keyframesを指定を0%と100%のみにすると、他の要素のtransformプロパティが効かなくなる。 対策としては、0%と50%と100%を指定する。(間に1つ挟むと大丈夫だった。 |
この他にも色々ありましたが、曖昧なものも混ざってるため記述しません。
また、他のサイトでも色々報告があがっていたので参考になりました。
・AndroidやiPhoneのHTML,CSS,JavaScriptのバグまとめ
http://blog.webcreativepark.net/2012/03/13-093853.html
・[css, css3, html] スマートフォン(iPhone, Android)ブラウザのバグまとめ
http://tenderfeel.xsrv.jp/css/1177/
http://blog.webcreativepark.net/2012/03/13-093853.html
・[css, css3, html] スマートフォン(iPhone, Android)ブラウザのバグまとめ
http://tenderfeel.xsrv.jp/css/1177/
終わりに
完全に、工数に見合わない作業でした。
Android2.3系だとAdobe EdgeとかSenchaAnimatorとかでもキレイに動かないし。。
実装時は、Android2.3系から確認して書いていくのが正。
いや、Android2.3系はサポート対象外にするのが正。
そうすると、みんな幸せに。。
Android2.3系だとAdobe EdgeとかSenchaAnimatorとかでもキレイに動かないし。。
実装時は、Android2.3系から確認して書いていくのが正。
いや、Android2.3系はサポート対象外にするのが正。
そうすると、みんな幸せに。。
以上!
登録:
投稿
(
Atom
)
0 件のコメント :
コメントを投稿