Macbook 12 (2016) で使えなくなったUSB-C関連機器

2016/4/20 突如、MacBook12(2016 Early)が発表されたので、早速、かごに投入しました。

今回購入したMacBook12(2016 Early)のスペックは、

ローズゴールド
1.3GHzデュアルコアIntel Core m7(Turbo Boost使用時最大3.1GHz)
512GB PCIeベースオンボードフラッシュストレージ
バックライトキーボード (JIS) + 製品マニュアル(日本語)

全部入りのCTOモデルにしました。

MacBook12(2015 Early)を使っていたので、USB-C周辺機器を2016モデルで使おうとしたら、
使えなかったので、使えない周辺機器の情報を共有します。

・使えた機器
Apple純正 USB-C – USBアダプタ
Apple純正 USB-C Digital AV Multiportアダプタ
Apple純正 USB-C VGA Multiportアダプタ
HyperDrive USB Type-C 5-in-1 Hub
HDMI変換アダプタ USB 3.1 タイプC to HDMIアダプター
Aukey Type-C USB-C USB3.0 4ポートハブ(C8-C17)
USB Type-Cコネクタ搭載USBハブ(U3HC-AP412B)

・使えなかった機器
Power Delivery USB-C to USB 3.0AF (USB PD USB-C Hub, Black)(PDのみ利用不可)
KanaaN カナーン MacBook 12″用 USB-C マルチポート アダプター(PDのみ利用不可)
Leesentec®Macbook対応充電Type-cアダプタ 両面挿し Type-c変換コネクター(認識されない)
USB 3.1 Type C to VGA 変換アダプター(認識されない)

使えない理由は、わかっていませんが、使えるデバイスが一気に減ったので、2015 Earlyで動いていた実績があったとしても、
2016 Earlyでは使えないことがあるってことは知っていた方がいいと思います。

以上、ご参考までに

MacBook 12インチ向け USB-C マルチポートアダプタのレビュー

MacBook 12を購入してから、様々なUSB-C関連製品を買っては、試しを繰り返していますが、やっと、充電しながら、USBポートが利用できたり、画面出力ができるサードパーティの周辺機器が出てきた。

今回は、MacBook 12″用 USB-C マルチポート アダプターを購入してみました。純正のUSB-C VGA Multiportアダプタと比較してどうなのか?をレビューしてみます。

    大きさ

IMG_5091
純正の方がコンパクトで小さいです。ただし、価格は4分の1。

    認識状態

まずは、純正のアダプタは、USB2.0-HUBの配下に、VGAモジュールを認識している状態であることがわかります。
151211-0001
今回のアダプタの場合は、USBバスには無いみたいです。
151211-0002
この違いが、ディスプレイの認識の仕方に違いが出ている模様。

    ディスプレイ接続時

純正のアダプタの場合は、Apple USB type-C VGA アダプタ経由で出力できていることが確認できます。
151211-0005

一方、このアダプタの場合は、アナログVGA または DVI-I経由のアナログで出力されていることが確認できます。
151211-0006

    注意点

このアダプタを利用して、ディスプレイ出力したい場合は、VGAケーブル(ディスプレイ)とこのアダプタを接続した状態で、MacBookにUSB-C経由で繋がないと、ディスプレイ出力されません。純正ケーブルはいつでもいいように設計されています。

    電源

充電しながら、USBポートが使えたり、ディスプレイ出力できています。純正ACアダプタを接続した際も29W電源と認識されます。

以上、ご参考までに

UPQ Phone A01の電池の残量が面白い

話題のスマホって言ったら、「iPhone 6S/6S Plus」だったりするけど、私的には、『UPQ Phone A01』だったりする。
ビックカメラで買えると発表になって、店頭で買えるならということで発売日に有楽町のビックカメラで買ったのであった。
Tweet Batteryのアップデートしたので、電池の残量をチェックしていたら、面白い現象を発見したので、ここに記す。
フル充電してから、ずっと起動したまま、Tweet Batteryで電池の残量をチェックし、45%を切ったら、1%毎に残量をTweetするようにして記録しています。

よく見ると、22%あたりから、残量が合っていないことに気づいたのか?慌てて、残量が減っていくのがわかります。これは面白い。機械なのに(笑)

Twitterのログ

@3zaru UPQASP001 UPQASP001 電池残量=45%,状態=充電していません 2015/10/20 午後5:38:19 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=44%,状態=充電していません 2015/10/20 午後6:26:55 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=43%,状態=充電していません 2015/10/20 午後7:19:03 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=42%,状態=充電していません 2015/10/20 午後8:04:38 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=41%,状態=充電していません 2015/10/20 午後9:28:15 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=40%,状態=充電していません 2015/10/20 午後10:13:13 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=39%,状態=充電していません 2015/10/20 午後10:58:46 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=38%,状態=充電していません 2015/10/20 午後11:40:06 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=37%,状態=充電していません 2015/10/20 午後11:42:07 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=36%,状態=充電していません 2015/10/21 午前0:29:24 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=35%,状態=充電していません 2015/10/21 午前1:20:38 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=34%,状態=充電していません 2015/10/21 午前2:45:24 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=33%,状態=充電していません 2015/10/21 午前3:41:55 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=32%,状態=充電していません 2015/10/21 午前4:36:26 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=31%,状態=充電していません 2015/10/21 午前5:10:49 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=30%,状態=充電していません 2015/10/21 午前5:48:46 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=29%,状態=充電していません 2015/10/21 午前6:40:46 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=28%,状態=充電していません 2015/10/21 午前7:37:47 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=27%,状態=充電していません 2015/10/21 午前8:25:58 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=26%,状態=充電していません 2015/10/21 午前9:18:37 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=25%,状態=充電していません 2015/10/21 午前10:01:23 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=24%,状態=充電していません 2015/10/21 午前10:46:51 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=23%,状態=充電していません 2015/10/21 午前11:23:53 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=22%,状態=充電していません 2015/10/21 午前11:44:44 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=21%,状態=充電していません 2015/10/21 午前11:46:29 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=20%,状態=充電していません 2015/10/21 午前11:47:12 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=19%,状態=充電していません 2015/10/21 午前11:47:55 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=18%,状態=充電していません 2015/10/21 午前11:48:39 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=17%,状態=充電していません 2015/10/21 午前11:49:11 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=16%,状態=充電していません 2015/10/21 午前11:49:32 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=15%,状態=充電していません 2015/10/21 午前11:50:08 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=14%,状態=充電していません 2015/10/21 午前11:50:50 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=13%,状態=充電していません 2015/10/21 午前11:51:33 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=12%,状態=充電していません 2015/10/21 午前11:52:08 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=11%,状態=充電していません 2015/10/21 午前11:52:51 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=10%,状態=充電していません 2015/10/21 午前11:53:35 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=9%,状態=充電していません 2015/10/21 午前11:54:19 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=8%,状態=充電していません 2015/10/21 午前11:54:51 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=7%,状態=充電していません 2015/10/21 午前11:55:12 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=6%,状態=充電していません 2015/10/21 午前11:55:47 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=5%,状態=充電していません 2015/10/21 午前11:56:29 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=4%,状態=充電していません 2015/10/21 午前11:57:13 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=3%,状態=充電していません 2015/10/21 午前11:57:47 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=2%,状態=充電していません 2015/10/21 午前11:58:31 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=1%,状態=充電していません 2015/10/21 午前11:59:13 #tweetbatt
@3zaru UPQASP001 UPQASP001 電池残量=0%,状態=充電していません 2015/10/21 午後0:00:28 #tweetbatt

USB Type-C対応HUB ( U3HC-AP412BWH ) を新型Macbookで使ってみる

新型Macbookを購入されたかたで、開発等で酷使される方は、USB-C関連周辺機器を色々と購入していると思われる。
新型Macbookには、USB-Cポートが一つしかなく、おそらく、USB-C対応のHUBが必要になる。
USB3.0/USB2.0ポートのついたHUBは、色々と出始めているが、USB-Cポートが付いた製品は、現時点では、U3HC-AP412BWHしかない状況である。

DSC_0747

USB-CコネクタのついたHUB以外の製品というと、外部ディスプレイに出力するための、USB-C Digital AV Multiportアダプタと、USB-C VGA Multiportアダプタです。
つなぐとどうなるのか?試してみた。

まずは、HUB単体で接続するとどのように認識されるのか?を見てみると

150625-0001

のように、USB2.0とUSB3.0のHUBとして認識された。
ここで、例えば、USB3.0対応のUSBメモリー(Sandisk Extreme)をつないで見ると

150625-0003

USB3.0 HUBの下に、Extremeが認識されます。

じゃあ、おまちかねの、USB-C Digital AV Multiportアダプタを接続してみると

150625-0006

USB2.0 HUBの下に、USB Type-C Digital AV Adapterが認識されていることが確認できますが、画面には出力されません。

HUB通さない場合はどうなるかというと!

150625-0007

のようになり、この時は当然、画面には出力されます。

OSX Yosemite (10.10.3)が、HUBを間にかますと出力されないようになっているのか?HUBが信号を流さないのか?わかりませんが、そういう状況です。

以上、USB Type-C対応HUB ( U3HC-AP412BWH ) の購入レポートでした。

覚書 Android Studioのアンインストール方法 (Mac版)

Android Studioのアンインストールで、Windows版はアンインストーラーで綺麗に削除できるが、Mac版は、Applicationフォルダからアプリを削除しても、SDKなど別のフォルダにインストールされているので、綺麗にならないってことで、コンソールを立ち上げて、下記のコマンドで削除していきます。

rm -Rf /Applications/Android\ Studio.app
rm -Rf ~/Library/Preferences/AndroidStudio*
rm ~/Library/Preferences/com.google.android.studio.plist
rm -Rf ~/Library/Application\ Support/AndroidStudio*
rm -Rf ~/Library/Logs/AndroidStudio*
rm -Rf ~/Library/Caches/AndroidStudio*
rm -Rf ~/.gradle
rm -Rf ~/.android

SDKのツールは

rm -Rf ~/Library/Android*

でOK!

WebRTC Data Channel Sample for Android を作ってみた

ここんところ、WebRTCのアプリを作ろうとして、四苦八苦していました。

やっと、データチャンネルで送受信するアプリができたので、ここに公開します。

開発環境ですが
Android Studio 1.2.1.1 build 141.19032520
を使って開発しています。

普通にプロジェクトを作成します。

まず、WebRTCのライブラリをインポートする為の設定を行います。

app配下にあるbuild.gradleのdependenciesに下記の一行を入れるだけ

compile 'io.pristine:libjingle:9357@aar'

これでなんと、ネイティブのsoライブラリまで持ってきてくれます(なんと便利な!)

150610-0006

次に、AndroidManifest.xmlにパーミッションを追加します

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:glEsVersion="0x00020000" android:required="true" />

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

150610-0003

これで、準備OK、ここからは、MainActivity.javaのソースをそのまま掲載しちゃいます。

package jp.eguchi.android.datachannelsample;

// WebRTC Data Channel Sample Program
// Programed by Kazuyuki Eguchi

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

import org.json.JSONObject;
import org.webrtc.DataChannel;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;

import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;


public class MainActivity extends Activity {

    final private String TAG = "sample";

    PeerConnectionFactory factory = null;

    PeerConnection pc1 = null;
    PeerConnection pc2 = null;

    DataChannel dc1 = null;
    DataChannel dc2 = null;

    // pc1用のObserver
    PeerConnection.Observer pcob1 = new PeerConnection.Observer() {
        @Override
        public void onSignalingChange(PeerConnection.SignalingState signalingState) {
            Log.d(TAG, "pcob1 onSignalingChange() " + signalingState.name());
        }

        @Override
        public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
            Log.d(TAG, "pcob1 onIceConnectionChange() " + iceConnectionState.name());
        }

        @Override
        public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
            Log.d(TAG, "pcob1 onIceGatheringChange() " + iceGatheringState.name());
        }

        @Override
        public void onIceCandidate(IceCandidate iceCandidate) {
            Log.d(TAG, "pcob1 onIceCandidate()");

            JSONObject json = new JSONObject();

            String mes = null;

            try {
                json.put("type", "candidate");
                json.put("sdpMLineIndex", iceCandidate.sdpMLineIndex);
                json.put("sdpMid", iceCandidate.sdpMid);
                json.put("candidate", iceCandidate.sdp);

                mes = json.toString();

                Log.d(TAG, mes);

                // ここで、 WebSocket等で相手側に mesを送る
                // 受信側では

                JSONObject json2 = new JSONObject(mes);
                IceCandidate candidate = new IceCandidate(json2.getString("sdpMid"), json2.getInt("sdpMLineIndex"), json2.getString("candidate"));
                pc2.addIceCandidate(candidate);

            } catch (org.json.JSONException ex) {
                Log.d(TAG, ex.toString());
            }
        }

        @Override
        public void onAddStream(MediaStream mediaStream) {
            Log.d(TAG, "pcob1 onAddStream()");
        }

        @Override
        public void onRemoveStream(MediaStream mediaStream) {
            Log.d(TAG, "pcob1 onRemoveStream");
        }

        @Override
        public void onDataChannel(DataChannel dataChannel) {
            Log.d(TAG, "pcob1 onDataChannel()");
        }

        @Override
        public void onRenegotiationNeeded() {
            Log.d(TAG, "pcob1 onRenegotiationNeeded()");
        }
    };

    // pc2用のObserver
    PeerConnection.Observer pcob2 = new PeerConnection.Observer() {
        @Override
        public void onSignalingChange(PeerConnection.SignalingState signalingState) {
            Log.d(TAG, "pcob2 onSignalingChange() " + signalingState.name());
        }

        @Override
        public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
            Log.d(TAG, "pcob2 onIceConnectionChange() " + iceConnectionState.name());
        }

        @Override
        public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
            Log.d(TAG, "pcob2 onIceGatheringChange() " + iceGatheringState.name());
        }

        @Override
        public void onIceCandidate(IceCandidate iceCandidate) {
            Log.d(TAG, "pcob2 onIceCandidate()");

            JSONObject json = new JSONObject();

            String mes = null;

            try {
                json.put("type", "candidate");
                json.put("sdpMLineIndex", iceCandidate.sdpMLineIndex);
                json.put("sdpMid", iceCandidate.sdpMid);
                json.put("candidate", iceCandidate.sdp);

                mes = json.toString();

                Log.d(TAG, mes);

                // ここで、 WebSocket等で相手側に mesを送る
                // 受信側では

                JSONObject json2 = new JSONObject(mes);
                IceCandidate candidate = new IceCandidate(json2.getString("sdpMid"), json2.getInt("sdpMLineIndex"), json2.getString("candidate"));
                pc1.addIceCandidate(candidate);

            } catch (org.json.JSONException ex) {
                Log.d(TAG, ex.toString());
            }
        }

        @Override
        public void onAddStream(MediaStream mediaStream) {
            Log.d(TAG, "pcob2 onAddStream()");
        }

        @Override
        public void onRemoveStream(MediaStream mediaStream) {
            Log.d(TAG, "pcob2 onRemoveStream");
        }

        @Override
        public void onDataChannel(DataChannel dataChannel) {
            Log.d(TAG, "pcob2 onDataChannel()");
            dc2 = dataChannel;
            dc2.registerObserver(dc2o);
        }

        @Override
        public void onRenegotiationNeeded() {
            Log.d(TAG, "pcob2 onRenegotiationNeeded()");
        }
    };

    SdpObserver so1 = new SdpObserver() {
        @Override
        public void onCreateSuccess(SessionDescription sessionDescription) {
            Log.d(TAG, "so1 onCreateSuccess()");

            pc1.setLocalDescription(so1, sessionDescription);

            JSONObject json = new JSONObject();

            String mes = null;

            try {
                json.put("type", sessionDescription.type.toString().toLowerCase());
                json.put("sdp", sessionDescription.description);

                mes = json.toString();

                Log.d(TAG, mes);

                // ここで、 WebSocket等で相手側に mesを送る
                // 受信側では

                JSONObject json2 = new JSONObject(mes);
                String type = json2.getString("type");
                String sdp = json2.getString("sdp");

                SessionDescription sdp2 = new SessionDescription(SessionDescription.Type.fromCanonicalForm(type), sdp);

                pc2.setRemoteDescription(so2, sdp2);
                MediaConstraints constraints = new MediaConstraints();
                pc2.createAnswer(so2, constraints);

            } catch (org.json.JSONException ex) {
                Log.d(TAG, ex.toString());
            }
        }

        @Override
        public void onSetSuccess() {
            Log.d(TAG, "so1 onCreateSuccess()");
        }

        @Override
        public void onCreateFailure(String s) {
            Log.d(TAG, "so1 onCreateFailure() " + s);
        }

        @Override
        public void onSetFailure(String s) {
            Log.d(TAG, "so1 onSetFailure() " + s);
        }
    };

    SdpObserver so2 = new SdpObserver() {
        @Override
        public void onCreateSuccess(SessionDescription sessionDescription) {
            Log.d(TAG, "so2 onCreateSuccess()");
            pc2.setLocalDescription(so2, sessionDescription);

            JSONObject json = new JSONObject();

            String mes = null;

            try {
                json.put("type", sessionDescription.type.toString().toLowerCase());
                json.put("sdp", sessionDescription.description);

                mes = json.toString();

                Log.d(TAG, mes);

                // ここで、 WebSocket等で相手側に mesを送る
                // 受信側では

                JSONObject json2 = new JSONObject(mes);
                String type = json2.getString("type");
                String sdp = json2.getString("sdp");

                SessionDescription sdp2 = new SessionDescription(SessionDescription.Type.fromCanonicalForm(type), sdp);

                pc1.setRemoteDescription(so1, sdp2);

            } catch (org.json.JSONException ex) {
                Log.d(TAG, ex.toString());
            }
        }

        @Override
        public void onSetSuccess() {
            Log.d(TAG, "so2 onCreateSuccess()");
        }

        @Override
        public void onCreateFailure(String s) {
            Log.d(TAG, "so2 onCreateFailure() " + s);
        }

        @Override
        public void onSetFailure(String s) {
            Log.d(TAG, "so2 onSetFailure() " + s);
        }
    };

    DataChannel.Observer dc1o = new DataChannel.Observer() {
        @Override
        public void onStateChange() {
            Log.d(TAG, "da1o onStateChange() " + dc1.state().name());

            if (dc1.state() == DataChannel.State.OPEN) {
                String data = "from dc1";
                ByteBuffer buffer = ByteBuffer.wrap(data.getBytes());
                dc1.send(new DataChannel.Buffer(buffer, false));
            }
        }

        @Override
        public void onMessage(DataChannel.Buffer buffer) {
            Log.d(TAG, "da1o onMessage()");

            if (buffer.binary == false) {
                int limit = buffer.data.limit();
                byte[] datas = new byte[limit];
                buffer.data.get(datas);
                String tmp = new String(datas);
                Log.d(TAG, tmp);
            }
        }
    };

    DataChannel.Observer dc2o = new DataChannel.Observer() {
        @Override
        public void onStateChange() {
            Log.d(TAG, "da2o onStateChange() " + dc2.state().name());

            if (dc2.state() == DataChannel.State.OPEN) {
                String data = "from dc2";
                ByteBuffer buffer = ByteBuffer.wrap(data.getBytes());
                dc2.send(new DataChannel.Buffer(buffer, false));
            }

        }

        @Override
        public void onMessage(DataChannel.Buffer buffer) {
            Log.d(TAG, "da2o onMessage()");

            if (buffer.binary == false) {
                int limit = buffer.data.limit();
                byte[] datas = new byte[limit];
                buffer.data.get(datas);
                String tmp = new String(datas);
                Log.d(TAG, tmp);
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d(TAG, "onCreate()");

        PeerConnectionFactory.initializeAndroidGlobals(getApplicationContext(), true, false, false, null);
        factory = new PeerConnectionFactory();
    }

    @Override
    protected void onResume() {
        super.onResume();

        Log.d(TAG, "onResume()");

        // STRNサーバは Google様のを使わせてもらう。
        List<PeerConnection.IceServer> iceServers = new LinkedList<PeerConnection.IceServer>();
        iceServers.add(new PeerConnection.IceServer("stun:stun.l.google.com:19302"));

        MediaConstraints constraints = new MediaConstraints();

        pc1 = factory.createPeerConnection(iceServers, constraints, pcob1);
        pc2 = factory.createPeerConnection(iceServers, constraints, pcob2);

        // offerする前にデータチャネルを作成する
        dc1 = pc1.createDataChannel("RTCDataChannel", new DataChannel.Init());
        dc1.registerObserver(dc1o);

        // Offerする
        pc1.createOffer(so1, constraints);
    }

    @Override
    protected void onPause() {
        super.onPause();

        Log.d(TAG, "onPause()");

        if (dc1 != null) {
            dc1.close();
            dc1.unregisterObserver();
        }

        if (dc2 != null) {
            dc2.close();
            dc2.unregisterObserver();
        }

        if (pc1 != null) {
            pc1.close();
        }

        if (pc2 != null) {
            pc2.close();
        }
    }
}

実行した際のLogcatのログ

06-10 17:44:58.719    4149-4149/jp.eguchi.android.datachannelsample D/sample﹕ onCreate()
06-10 17:44:58.723   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ AttachCurrentThreadIfNeeded::ctor@[tid=11577]
06-10 17:44:58.723   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ Attaching thread to JVM
06-10 17:44:58.724   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ JVM::environment@[tid=11577]
06-10 17:44:58.724   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ JNIEnvironment::ctor@[tid=11577]
06-10 17:44:58.724   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ ctor@[tid=11577]
06-10 17:44:58.724   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ JNIEnvironment::RegisterNatives(org/webrtc/voiceengine/WebRtcAudioManager)
06-10 17:44:58.724   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ NativeRegistration::ctor@[tid=11577]
06-10 17:44:58.724   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ NativeRegistration::NewObject@[tid=11577]
06-10 17:44:58.724   4149-11577/jp.eguchi.android.datachannelsample D/WebRtcAudioManager﹕ ctor@[name=Thread-1038, id=1038]
06-10 17:44:58.724   4149-11577/jp.eguchi.android.datachannelsample D/WebRtcAudioManager﹕ Nexus 5 is blacklisted for HW AEC usage!
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ OnCacheAudioParameters@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ hardware_aec: 0
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ low_latency_output: 1
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ sample_rate: 48000
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ channels: 1
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ output_buffer_size: 240
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ input_buffer_size: 1920
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ GlobalRef::ctor@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ JavaAudioManager::ctor@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ IsLowLatencyPlayoutSupported()
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample W/AudioManager﹕ NOTE: OpenSL ES output is currently disabled!
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ AttachCurrentThreadIfNeeded::ctor@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ JVM::environment@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ JNIEnvironment::ctor@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/AudioTrackJni﹕ ctor@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ JNIEnvironment::RegisterNatives(org/webrtc/voiceengine/WebRtcAudioTrack)
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ NativeRegistration::ctor@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ NativeRegistration::NewObject@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/WebRtcAudioTrack﹕ ctor@[name=Thread-1038, id=1038]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/WebRtcAudioTrack﹕ Android SDK: 22, Release: M, Brand: google, Device: hammerhead, Id: MPZ44Q, Hardware: hammerhead, Manufacturer: LGE, Model: Nexus 5, Product: hammerhead
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ GlobalRef::ctor@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ AttachCurrentThreadIfNeeded::ctor@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ JVM::environment@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ JNIEnvironment::ctor@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/AudioRecordJni﹕ ctor@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ JNIEnvironment::RegisterNatives(org/webrtc/voiceengine/WebRtcAudioRecord)
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ NativeRegistration::ctor@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ NativeRegistration::NewObject@[tid=11577]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/WebRtcAudioRecord﹕ ctor@[name=Thread-1038, id=1038]
06-10 17:44:58.726   4149-11577/jp.eguchi.android.datachannelsample D/JVM﹕ GlobalRef::ctor@[tid=11577]
06-10 17:44:58.727   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ SetActiveAudioLayer(5)@[tid=11577]
06-10 17:44:58.727   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ delay_estimate_in_milliseconds: 150
06-10 17:44:58.727   4149-11577/jp.eguchi.android.datachannelsample D/AudioTrackJni﹕ AttachAudioBuffer@[tid=11577]
06-10 17:44:58.727   4149-11577/jp.eguchi.android.datachannelsample D/AudioTrackJni﹕ SetPlayoutSampleRate(48000)
06-10 17:44:58.727   4149-11577/jp.eguchi.android.datachannelsample D/AudioTrackJni﹕ SetPlayoutChannels(1)
06-10 17:44:58.727   4149-11577/jp.eguchi.android.datachannelsample D/AudioRecordJni﹕ AttachAudioBuffer
06-10 17:44:58.727   4149-11577/jp.eguchi.android.datachannelsample D/AudioRecordJni﹕ SetRecordingSampleRate(48000)
06-10 17:44:58.727   4149-11577/jp.eguchi.android.datachannelsample D/AudioRecordJni﹕ SetRecordingChannels(1)
06-10 17:44:58.727   4149-11577/jp.eguchi.android.datachannelsample D/AudioRecordJni﹕ total_delay_in_milliseconds: 150
06-10 17:44:58.727   4149-11577/jp.eguchi.android.datachannelsample D/AudioManager﹕ Init@[tid=11577]
06-10 17:44:58.727   4149-11577/jp.eguchi.android.datachannelsample D/WebRtcAudioManager﹕ init@[name=Thread-1038, id=1038]
06-10 17:44:58.731    4149-4149/jp.eguchi.android.datachannelsample D/sample﹕ onResume()
06-10 17:44:59.157   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob1 onRenegotiationNeeded()
06-10 17:44:59.158   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ so1 onCreateSuccess()
06-10 17:44:59.395   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob1 onSignalingChange() HAVE_LOCAL_OFFER
06-10 17:44:59.396   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ {"type":"offer","sdp":"v=0\r\no=- 113918549713234061 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=msid-semantic: WMS\r\nm=application 9 DTLS\/SCTP 5000\r\nc=IN IP4 0.0.0.0\r\na=ice-ufrag:lw3\/LpT+Giz61KGg\r\na=ice-pwd:QxRiyQgLEFnfWEsJ0p5+6G3s\r\na=fingerprint:sha-256 B8:AB:2D:09:BB:01:23:04:44:64:81:1E:F6:65:24:25:74:A5:6C:F0:62:8B:96:3E:86:83:E2:ED:A9:33:E6:FA\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"}
06-10 17:44:59.398   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob2 onSignalingChange() HAVE_REMOTE_OFFER
06-10 17:44:59.398   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob1 onIceGatheringChange() GATHERING
06-10 17:44:59.398   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ so1 onCreateSuccess()
06-10 17:44:59.398   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ so2 onCreateSuccess()
06-10 17:44:59.399   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ so2 onCreateSuccess()
06-10 17:44:59.404   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob2 onSignalingChange() STABLE
06-10 17:44:59.406   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ {"type":"answer","sdp":"v=0\r\no=- 3088308566766388678 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=msid-semantic: WMS\r\nm=application 9 DTLS\/SCTP 5000\r\nc=IN IP4 0.0.0.0\r\nb=AS:30\r\na=ice-ufrag:4gco7NuR3wheyF6s\r\na=ice-pwd:Fv95\/TF5FvTbGMCcISNSKNf3\r\na=fingerprint:sha-256 31:EE:E9:19:85:5B:02:5F:CC:80:A3:FC:F7:64:EC:71:B6:01:4E:D0:35:1B:74:8E:D1:30:05:43:49:0D:52:55\r\na=setup:active\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"}
06-10 17:44:59.410   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob1 onSignalingChange() STABLE
06-10 17:44:59.412   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob1 onIceConnectionChange() CHECKING
06-10 17:44:59.412   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob1 onIceCandidate()
06-10 17:44:59.412   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ {"type":"candidate","sdpMLineIndex":0,"sdpMid":"data","candidate":"candidate:3209757399 1 udp 2122260223 10.49.120.146 39965 typ host generation 0"}
06-10 17:44:59.415   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob2 onIceConnectionChange() CHECKING
06-10 17:44:59.415   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob2 onIceGatheringChange() GATHERING
06-10 17:44:59.415   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ so2 onCreateSuccess()
06-10 17:44:59.415   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ so1 onCreateSuccess()
06-10 17:44:59.416   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob2 onIceCandidate()
06-10 17:44:59.416   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ {"type":"candidate","sdpMLineIndex":0,"sdpMid":"data","candidate":"candidate:3209757399 1 udp 2122260223 10.49.120.146 41101 typ host generation 0"}
06-10 17:44:59.444   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob1 onIceGatheringChange() COMPLETE
06-10 17:44:59.453   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob2 onIceGatheringChange() COMPLETE
06-10 17:44:59.486   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob1 onIceConnectionChange() CONNECTED
06-10 17:44:59.487   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob1 onIceConnectionChange() COMPLETED
06-10 17:44:59.488   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ da1o onStateChange() OPEN
06-10 17:44:59.489   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob2 onIceConnectionChange() CONNECTED
06-10 17:44:59.493   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob2 onDataChannel()
06-10 17:44:59.494   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ da2o onStateChange() OPEN
06-10 17:44:59.495   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ da2o onMessage()
06-10 17:44:59.495   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ from dc1
06-10 17:44:59.496   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ da1o onMessage()
06-10 17:44:59.496   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ from dc2
06-10 17:45:03.231    4149-4149/jp.eguchi.android.datachannelsample D/sample﹕ onPause()
06-10 17:45:03.231   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ da1o onStateChange() CLOSING
06-10 17:45:03.233   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ da1o onStateChange() CLOSED
06-10 17:45:03.233   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ da2o onStateChange() CLOSING
06-10 17:45:03.234   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ da2o onStateChange() CLOSED
06-10 17:45:03.236   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob1 onIceConnectionChange() CLOSED
06-10 17:45:03.236   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob1 onSignalingChange() CLOSED
06-10 17:45:03.239   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob2 onIceConnectionChange() CLOSED
06-10 17:45:03.240   4149-11578/jp.eguchi.android.datachannelsample D/sample﹕ pcob2 onSignalingChange() CLOSED

以上、ご参考までに

これで、WebRTCのアプリが増えるといいなあ〜

WebRTCを Androidネイティブアプリに実装する

ワイヤレスジャパン2015で、『メイドさんの吐息を地球の裏側に転送する』なんてタイトルで発表して、記事『メイドさんの吐息を感じて、タッチ――「日本Androidの会」秋葉原支部セッション』にしていただいた。

私が作成したデバイス『ソーシャルうちわ』は、1対多の通信を行うので、WebSocketを採用していたが、アクセサリ部のメンバーが作成した吐息を伝送するデバイスは1対1の通信であるため、WebRTCを採用している。
自分が作っているデバイスは必ずしも、インターネットのサーバ経由で通信しなくてもいい場合もあるので、AndroidのアプリにWebRTCを実装する必要性もあるので、実装方法を調査していた。実際やってみると、苦難の連続だったので、ここに覚書を示す。

Googleで実装方法を検索してみると、WebRTCの公式ページにサンプルがあるのでこれをベースにやっているようなので、サンプルがコンパイルでき、実機で動くまでをやってみる。

まず、躓いたことを先に書くと、Max OSX Yosemite上で、コンパイルしようとしたら、うまく行かなかった。
なので、WebRTCなAndroidネイティブアプリケーションについてに書いてあるように、Linux上でコンパイルすることにした。

まずは、自分が実際にやった環境のご紹介

Mac Book Air (2013 Mid)
OS X Yosemite(10.10.3)
Paralleses Desktop上で、Ubuntu 14.04.2 LTSをインストールして、ログイン後ターミナルを立ち上げた状態から、やったことを書いてみる。(メモリーは2GBを割り当てました)

まずは、お約束のアップデート

sudo apt-get update
sudo apt-get upgrade

ビルドする際に必要なパッケージをインストールする

sudo apt-get install git subversion openjdk-7-jdk
export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64

みたいなんですが、途中で、c++のコンパイラーが必要なので

sudo apt-get install g++

をやっておきましょう。

その後は

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=`pwd`/depot_tools:"$PATH"
export GYP_DEFINES="OS=android"
fetch webrtc_android

で、ソースコードを取得できます。この作業はめっちゃ時間(1時間以上)がかかりますので。忍耐強く待ちましょう。途中で止めたり(Ctrl+C)した場合は、

gclient sync

で、再開させましょう。

cd src
. build/android/envsetup.sh
export GYP_DEFINES="build_with_libjingle=1 build_with_chromium=0 libjingle_java=1 enable_android_opensl=0 $GYP_DEFINES"
gclient runhooks

で、コンパイルをすることになるのですが、よくある手段をやると、コンパイルが途中で止まるので、その前に必要なコンポーネントのインストールをしましょう。

./build/install-build-deps-android.sh

インストールが終わったら、忍者を呼びましょう

ninja -C out/Release -j2 AppRTCDemo

これで、コンパイルすることが出来ました。
サンプルアプリのapkファイルは、/src/out/Release/AppRTCDemo.apkから取得してください。
これを、OSX上に持ってきて、実機にインストールするとサンプルアプリを動かすことが出来ます。
2015/06/02現在で、apkできた方法を紹介しました!

ご参考までに

node.jsスクリプトの自動起動

覚書な為、詳しくは書いていません。

Raspberry pi 2上で動作するnode.jsアプリを起動時に自動起動するためやり方。

sudo npm install -g forever

/etc/rc.local に下記の行を追記する


sudo -u pi /usr/bin/node /usr/bin/forever start -p /var/run/forever --pidfile /var/run/node-app.pid -l /home/pi/node-app.log -a -d /home/pi/aaa/aaa.js

※sudo で、ユーザを指定して起動するのがコツ

Raspberry pi 2 購入 その6 VNCサーバ(覚書)

基本は、SSH接続できれば十分な気もするが、VNC接続できるようにサーバを設定する

sudo apt-get install tightvncserver

で、パッケージをインストール。

パスワードを設定する。短いパスワードだと設定出来ないので、長めに!

vncpasswd

VNCサーバを起動します。

vncserver :0 -geometry 1024×600 -depth 16

ディスプレイ番号0番(5900ポート)で、解像度1024×600、色数は16bitっていう設定になります。

これで、VNCクライアントから接続確認がとれたら、自動起動するように設定する。

/etc/init.d/vncboot
っていうファイルを新規に作成する

### BEGIN INIT INFO
# Provides: vncboot
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start VNC Server at boot time
# Description: Start VNC Server at boot time.
### END INIT INFO

#! /bin/sh
# /etc/init.d/vncboot

USER=pi
HOME=/home/pi

export USER HOME

case "$1" in
    start)
        echo "Starting VNC Server"
        #Insert your favoured settings for a VNC session
        su $USER -c '/usr/bin/vncserver :0 -geometry 1024x600 -depth 16'
        ;;

    stop)
        echo "Stopping VNC Server"
        su $USER -c '/usr/bin/vncserver -kill :1'
        ;;

    *)
        echo "Usage: /etc/init.d/vncboot {start|stop}"
        exit 1
        ;;
esac

exit 0

管理者権限を追加します。

sudo chmod 755 /etc/init.d/vncboot

自動起動の設定を行う。

sudo update-rc.d vncboot defaults

これで、再起動させて、クライアントから、VNC接続ができればOK!

Raspberry pi 2 購入 その5 固定IPアドレス(覚書)

サーバとして利用したいので、無線LANのIPをDHCPから固定IPに設定する

/etc/network/interface

auto lo
iface lo inet loopback
allow-hotplug eth0
iface eth0 inet dhcp

allow-hotplug wlan0
iface wlan0 inet manual
wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
iface default inet dhcp

を下記のように編集する

auto lo

iface lo inet loopback
iface eth0 inet dhcp

allow-hotplug wlan0
iface wlan0 inet static
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
address 192.168.100.10
network 192.168.100.0
netmask 255.255.255.0
broadcast 192.168.100.255
gateway 192.168.100.1
dns-nameservers 192.168.100.1

iface default inet dhcp

8行目はもともと、wpa-roamになっているので、これをwpa-confに設定しないと無線LANの接続は行われないので注意