實做Signaling Server與WebRTC 連接前之交互流程

最近拿Socket.io來實做Signaling Server, 做為WebRTC連接前溝通的信令伺服器, 這篇就來紀錄一下整個過程.

先來一些簡單小問題解惑:

1. 什麼是WebRTC呢?

其實之前有寫過一篇 Learn AppRTC/WebRTC 可以先看看.

他有包含三個部份:

  1. RTCPeerConnection, 主要是在建立雙方連接的API, 連接完後就會使用下面兩個API傳輸資料
  2. Network Stream API, 設定傳輸媒體流使用(Video/Audio)
  3. Peer-to-Peer Data API, 這邊就是你可以拿來傳送資料的API, 在這條路上你要傳送什麼隨便你. 最重要的就是RTCPeerConnection在Android上就是PeerConnection這個Class

2. Signaling Server作用是什麼?

  1. 因為WebRTC是一種P2P概念的東西, 在剛開始雙方要連線時, 根本不認識對方, 也不知道對方在何處, 所以中間一定要有一台Server幫雙方建立剛開始相互認識的橋樑
  2. 深入一點說, 這邊Signaling Server要幫兩台設備轉傳一些資料, 例如 SessionDescription (SDP), Ice candidate…這些轉傳完兩台設備就會互向認識, 互相跨越對方防火牆, 但不是所有NAT Type都試用, 若至少有一人是NAT Type 4 (對稱型的NAT)的話就必須要多一台Turn Server, 不然基本上Stun Server就夠了.

3. 為什麼會拿Socket.io來實做Signaling Server呢?

  1. 因為他的Library非常好上手, 而且有支持各個平台(瀏覽器/IOS/Android/…), 所以就拿來玩玩看, 這邊是使用Socket.io官方提供的Chat Demo來改, 有興趣也可以去玩玩看這個Demo很好玩.
  2. 另外我實在懶的去找其他的工具來用了…

開始我們的主題,

先講一下WebRTC要怎麼樣雙方才會對上. 首先有兩個Peer, 假設是Peer A 跟 Peer B好了

總是有個人會先上線吧?

Peer A 先上線了!!

  1. Peer A call createOffer.
  2. Peer A 等待onCreateSuccess
  3. Peer A 等到onCreateSuccess後會拿到 SDP, 把SDP 拿去setLocalSdp.( 自己的嘛, 當然是setLocal囉)
  4. Peer A 把Sdp送到server上放著
  5. Peer A 收集onIceCandidate的IceCandidates, 然後等待Peer B出現

Peer B上線了!!!

  1. Peer B 到Server上找尋Peer A的身影, 拿取Sdp, 將Sdp拿去setRemoteSdp(別人的嘛, 當然是setRemote囉)
  2. 因為Peer B已經拿到Offer了, 要給點回應 所以 call createAnswer 並且等待onCreateSucess
  3. Peer B有Offer Sdp了想要更進一步拿到Peer A的IceCandidates, 於是發訊息跟Server請Peer A把IceCandidates都透過Server送過來
  4. 等待Peer B收到Peer A的IceCandidates把他全部addIceCandidate.
  5. Peer B 等到onCreateSuccess後也會拿到Sdp, 把Sdp拿去setLocalSdp. 並且透過Server送給Peer A, Peer A收到後要 setRemoteSdp.
  6. Peer B 在onIceCandidate時將IceCandidate透過Server轉發給Peer A, 然後Peer A要 addIceCandidate.

接下來兩個人就不用在透過Server了…(Server表示傷心)


理論上是還有一台Server要幫忙配對, 這台在官方就叫做Room Server,兩個人進同一間房間就是一種配對, 這邊直接在Signaling Server中讓雙方都帶同一個username去取代這間事情.

Mar 31st, 2017

JNI C Register Java Callback


Java可以透過JNI去跟其他語言溝通,這邊要介紹跟C溝通的一種情境,這篇不是教人如何使用JNI去Call C的function,而是反過來想要使用C 去 Call Java 的function,而且還是當作Callback的情況下使用。

(今天你想要開發一個Java library, 但是你已經有C版本的library了,你現在想要使用C library當作核心,多寫一份JNI的code讓他可以延伸到Java的世界來,這時候有些callback function要接到java去就可以使用這種方法)

我在改Google的AppRTC的時候看到了libjingle_peerconnection.jar中有個DataChannel的Class 其中有個Interface是Observer,這個Observer是要留給使用者實作,而這個Observer中的

  1. onMessage是在接收到DataChannel的資料的時候會被Call到
  2. onStateChange是在DataChannel的狀態有改變的時候會被Call到

使用者要自行去Implement這個DataChannel.Observer,畢竟開發Library的人不知道你想要在DataChannel狀態改變的時候做什麼跟你想要把接收到的資料做什麼,所以就留了一個空間讓大家去發揮。

然而在implement完這個DataChannel.Observer的時候,我要如何去告知底層的C,跟他說Callback function設定成我Implement好的這個DataChannel.Observer Class呢。

這邊就使用了registerObserver這個function,這個function其實最後是通到了native,把implement好的實體帶往C,讓C記住之後,當C要用的時候就Call就好了想法就這麼簡單。

( Note: libjingle_peerconnection.jar只是libjingle.so在java層的一層殼子,所有的事情都是在native中完成,然後再通知java這邊做些應對,所以必須要跟native層說java層這邊準備好了什麼你記得來Call,用Interface的原因是因為要限制接口的樣子,畢竟他是library,使用者要依照他的形式下去使用。)

下面大概描述一下要如何使用這樣的機制:

library_name.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class library_name {
  static {
      // if library .so is named 'libname.so'
      System.loadLibrary("name")
  }
  //跟Native註冊interface_name實體的Native function.
  private native registerInterfaceNameNative(interface_name var1);
      
  //假設有一個叫做interface_name的 Interface
  public Interface interface_name {
      void interface_func(byte[]);
  }
  //java的接口,負責把interface_name的實體帶給Native function.
  public void registerInterfaceName(interface_name var1) {
      registerInterfaceNameNative(var1);
  }
}
library_jni.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static JavaVM *gJavaVM;
jobject callback_obj;
jmethodID callback_mid;

JNIEXPORT jint JNICALL Java_com_XXXXX_library_name_somethingInitFunction(JNIEnv* env, jobject obj) {
  (*env)->GetJavaVM(env,&gJavaVM);
}

JNIEXPORT jint JNICALL Java_com_XXXXX_library_name_registerInterfaceNameNative(JNIEnv* env, jobject obj, jobject instance) {
  callback_obj = (*env)->NewGlobalRef(env,instance);
  jclass clz = (*env)->GetObjectClass(env,callback_obj);
  if(clz == NULL) {
      //failed to find class
  }
  callback_mid = (*env)->GetMethodID(env,clz,"interface_func","([B)V");
}

// callback function in C
void cb_function(char* buf) {
  JNIEnv *env;
  if((*gJavaVM)->AttachCurrentThread(gJavaVM, &env, NULL) != JNI_OK)    {
      // attachCurrentThread() failed.
  }
  else
  {
        // create byte[]
        jbyteArray arr = (*env)->NewByteArray(env,len);
        // set buf -> byte[]
        (*env)->SetByteArrayRegion(env,arr,0,len, (jbyte*)buf);
        // call java function, which is implemented by user
        (*env)->CallVoidMethod(env,callback_obj,callback_mid,arr);
  }
}

實際使用上,要先實作 library_name.interface_name 然後把實作好的實體new出來後從registerInterfaceName一路帶下去給C.

大概會長成這樣

used_library.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class interfaceImpl implements library_name.interface_name {
  @Override
  void interface_func(byte[] a) {
      // show new String(a);
  }
}

 main {
  library_name library = new library_name();
  // call some function to init library
  // ...
  
  //這行最重要,要把implmenet完的實體送下去給Native層知道
  library.registerInterfaceName(new interfaceImpl());

  ...
}

這樣就可以透過註冊的方式設定Native C可以用自己implement的Interface callback回來囉.

大致上是這樣子使用,我也還在學習中,

如果有問題歡迎交流:D

Nov 2nd, 2015

Following Google Drive Quick Start (Android)


這其實就是Follow官網的Android Quickstart下去實作,畢竟前一陣子我耐不下心看官網教學我就一直做不出來…,寫成中文的應該相對簡單。

  • Step1. 獲得 SHA1 FingerPrint

    使用keytool (keytool一般就在java_path/bin下面),找到他然後開cmd(terminal),在有keytool的目錄下輸入command

    • windows 要下:(ex: 我的電腦名稱是dell11,理論上你可以放在你想要的位置,我是乖乖照著官網想要的路徑擺放)

        $keytool -exportcert -alias androiddebugkey -keystore "C:\Users\dell11\.android\debug.keystore" -list -v
      
    • Mac/Linux 要下:

        $keytool -exportcert -alias androiddebugkey -keystore "~/.android/debug.keystore" -list -v
      

    差別大概就是在斜線方向不同,還有作業系統file system的一些差異,輸入了之後他會叫你輸入password,此時輸入 android 便可,然後應該可以看到一串東西如下:

    輸入金鑰儲存庫密碼:
    別名名稱: androiddebugkey
    建立日期: 2015/4/14
    項目類型: PrivateKeyEntry
    憑證鏈長度: 1
    憑證 [1]:
    擁有者: CN=Android Debug, O=Android, C=US
    發出者: CN=Android Debug, O=Android, C=US
    序號: 552bf95a
    有效期自: Tue Apr 14 01:14:02 CST 2015 到: Thu Apr 06 01:14:02 CST 2045
    憑證指紋:
         MD5:  D8:C1:94:74:C7:0F:91:D8:EC:06:54:33:43:D3:EC:9F
         SHA1: 27:82:C6:F3:FB:9A:82:2D:A5:93:36:3B:31:A9:F5:98:4A:0D:33:2C
         SHA256: 95:A0:D3:69:DA:E6:37:BB:B0:66:9E:F2:D3:16:0D:8F:F4:E6:92:0D:B0:90:1D:A7:51:0C:87:7A:98:70:40:05
         簽章演算法名稱: SHA1withRSA
         版本: 3    
    

    把SHA1那行給記起來(D8:AA:43:97:59:EE:C5:95:26:6A:07:EE:1C:37:8E:F4:F0:C8:05:C8)。

  • Step2. 開啟Google API

    然後到這裡,選擇建立 新專案 => 繼續 => 前往憑證 => 新增憑證 => 選擇OAuth2.0用戶端編號 => 你會發現不能選擇應用程式類型,因為Google說 您必須先在同意畫面中設定產品名稱,才能建立 OAuth 用戶端編號,所以先去設定產品名稱之後,就可以回來選擇應用程式類型, 這邊選擇Android:
    • 用戶端名稱 : 打什麼應該都可以。
    • 簽署憑證的指紋 : 就填上剛剛keytool拿到的SHA1後面那一長串。
    • 套件名稱 : 這個很重要,應用程式建構後要跟這個值對到才可以使用google drive api,例如你的應用程式是叫做 com.example.drivequickstart,這邊也要輸入com.example.drivequickstart. (為了實作方便,直接用這個吧,Android 提供的Source Code也是用這個) 然後選擇建立
  • Step3. 建立一個新的Android 應用程式專案

    打開Android Studio,開一個新專案(Company Domain : com.example.drivequickstart) ,然後把build.gradle(app)內容換成:

    apply plugin: 'com.android.application'
    
    android {
        compileSdkVersion 22
        buildToolsVersion "22.0.1"
    
        defaultConfig {
            applicationId "com.example.drivequickstart"
            minSdkVersion 11
            targetSdkVersion 22
            versionCode 1
            versionName "1.0"
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
            }
        }
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.android.support:appcompat-v7:22.1.1'
        compile 'com.google.android.gms:play-services:7.3.0'
        compile 'com.google.api-client:google-api-client:1.20.0'
        compile 'com.google.api-client:google-api-client-android:1.20.0'
        compile 'com.google.api-client:google-api-client-gson:1.20.0'
        compile 'com.google.apis:google-api-services-drive:v2-rev170-1.20.0'
        } 
    

    然後在上方列點選 Android=> Sync Project with Gradle Files. 讓他把依賴的sdk加入project中,然後分別覆蓋下列幾個檔案,如果不存在就開新檔 :

  • AndroidManifest.xml
AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.drivequickstart">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
    <uses-permission android:name="android.permission.USE_CREDENTIALS" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="Drive API Android Quickstart"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="Drive API Android Quickstart" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
    </application>
</manifest>
  • MainActivity.java
MainActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
package com.example.drivequickstart;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.util.ExponentialBackOff;


import com.google.api.services.drive.DriveScopes;

import android.accounts.AccountManager;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Typeface;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.method.ScrollingMovementMethod;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import java.util.Arrays;
import java.util.List;

public class MainActivity extends Activity {
    /**
     * A Drive API service object used to access the API.
     * Note: Do not confuse this class with API library's model classes, which
     * represent specific data structures.
     */
    com.google.api.services.drive.Drive mService;

    GoogleAccountCredential credential;
    private TextView mStatusText;
    private TextView mResultsText;
    ProgressDialog mProgress;
    final HttpTransport transport = AndroidHttp.newCompatibleTransport();
    final JsonFactory jsonFactory = GsonFactory.getDefaultInstance();

    static final int REQUEST_ACCOUNT_PICKER = 1000;
    static final int REQUEST_AUTHORIZATION = 1001;
    static final int REQUEST_GOOGLE_PLAY_SERVICES = 1002;
    private static final String PREF_ACCOUNT_NAME = "accountName";
    private static final String[] SCOPES = { DriveScopes.DRIVE_METADATA_READONLY };

    /**
     * Create the main activity.
     * @param savedInstanceState previously saved instance data.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout activityLayout = new LinearLayout(this);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.MATCH_PARENT);
        activityLayout.setLayoutParams(lp);
        activityLayout.setOrientation(LinearLayout.VERTICAL);
        activityLayout.setPadding(16, 16, 16, 16);

        ViewGroup.LayoutParams tlp = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);

        mStatusText = new TextView(this);
        mStatusText.setLayoutParams(tlp);
        mStatusText.setTypeface(null, Typeface.BOLD);
        mStatusText.setText("Retrieving data...");
        activityLayout.addView(mStatusText);

        mResultsText = new TextView(this);
        mResultsText.setLayoutParams(tlp);
        mResultsText.setPadding(16, 16, 16, 16);
        mResultsText.setVerticalScrollBarEnabled(true);
        mResultsText.setMovementMethod(new ScrollingMovementMethod());
        activityLayout.addView(mResultsText);

        mProgress = new ProgressDialog(this);
        mProgress.setMessage("Calling Drive API ...");

        setContentView(activityLayout);

        // Initialize credentials and service object.
        SharedPreferences settings = getPreferences(Context.MODE_PRIVATE);
        credential = GoogleAccountCredential.usingOAuth2(
                getApplicationContext(), Arrays.asList(SCOPES))
                .setBackOff(new ExponentialBackOff())
                .setSelectedAccountName(settings.getString(PREF_ACCOUNT_NAME, null));

        mService = new com.google.api.services.drive.Drive.Builder(
                transport, jsonFactory, credential)
                .setApplicationName("Drive API Android Quickstart")
                .build();
    }


    /**
     * Called whenever this activity is pushed to the foreground, such as after
     * a call to onCreate().
     */
    @Override
    protected void onResume() {
        super.onResume();
        if (isGooglePlayServicesAvailable()) {
            refreshResults();
        } else {
            mStatusText.setText("Google Play Services required: " +
                    "after installing, close and relaunch this app.");
        }
    }

    /**
     * Called when an activity launched here (specifically, AccountPicker
     * and authorization) exits, giving you the requestCode you started it with,
     * the resultCode it returned, and any additional data from it.
     * @param requestCode code indicating which activity result is incoming.
     * @param resultCode code indicating the result of the incoming
     *     activity result.
     * @param data Intent (containing result data) returned by incoming
     *     activity result.
     */
    @Override
    protected void onActivityResult(
            int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch(requestCode) {
            case REQUEST_GOOGLE_PLAY_SERVICES:
                if (resultCode != RESULT_OK) {
                    isGooglePlayServicesAvailable();
                }
                break;
            case REQUEST_ACCOUNT_PICKER:
                if (resultCode == RESULT_OK && data != null &&
                        data.getExtras() != null) {
                    String accountName =
                            data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
                    if (accountName != null) {
                        credential.setSelectedAccountName(accountName);
                        SharedPreferences settings =
                                getPreferences(Context.MODE_PRIVATE);
                        SharedPreferences.Editor editor = settings.edit();
                        editor.putString(PREF_ACCOUNT_NAME, accountName);
                        editor.commit();
                    }
                } else if (resultCode == RESULT_CANCELED) {
                    mStatusText.setText("Account unspecified.");
                }
                break;
            case REQUEST_AUTHORIZATION:
                if (resultCode != RESULT_OK) {
                    chooseAccount();
                }
                break;
        }

        super.onActivityResult(requestCode, resultCode, data);
    }

    /**
     * Attempt to get a set of data from the Drive API to display. If the
     * email address isn't known yet, then call chooseAccount() method so the
     * user can pick an account.
     */
    private void refreshResults() {
        if (credential.getSelectedAccountName() == null) {
            chooseAccount();
        } else {
            if (isDeviceOnline()) {
                mProgress.show();
                new ApiAsyncTask(this).execute();
            } else {
                mStatusText.setText("No network connection available.");
            }
        }
    }

    /**
     * Clear any existing Drive API data from the TextView and update
     * the header message; called from background threads and async tasks
     * that need to update the UI (in the UI thread).
     */
    public void clearResultsText() {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mStatusText.setText("Retrieving data…");
                mResultsText.setText("");
            }
        });
    }

    /**
     * Fill the data TextView with the given List of Strings; called from
     * background threads and async tasks that need to update the UI (in the
     * UI thread).
     * @param dataStrings a List of Strings to populate the main TextView with.
     */
    public void updateResultsText(final List<String> dataStrings) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (dataStrings == null) {
                    mStatusText.setText("Error retrieving data!");
                } else if (dataStrings.size() == 0) {
                    mStatusText.setText("No data found.");
                } else {
                    mStatusText.setText("Data retrieved using" +
                            " the Drive API:");
                    mResultsText.setText(TextUtils.join("\n\n", dataStrings));
                }
            }
        });
    }

    /**
     * Show a status message in the list header TextView; called from background
     * threads and async tasks that need to update the UI (in the UI thread).
     * @param message a String to display in the UI header TextView.
     */
    public void updateStatus(final String message) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mStatusText.setText(message);
            }
        });
    }

    /**
     * Starts an activity in Google Play Services so the user can pick an
     * account.
     */
    private void chooseAccount() {
        startActivityForResult(
                credential.newChooseAccountIntent(), REQUEST_ACCOUNT_PICKER);
    }

    /**
     * Checks whether the device currently has a network connection.
     * @return true if the device has a network connection, false otherwise.
     */
    private boolean isDeviceOnline() {
        ConnectivityManager connMgr =
                (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
        return (networkInfo != null && networkInfo.isConnected());
    }

    /**
     * Check that Google Play services APK is installed and up to date. Will
     * launch an error dialog for the user to update Google Play Services if
     * possible.
     * @return true if Google Play Services is available and up to
     *     date on this device; false otherwise.
     */
    private boolean isGooglePlayServicesAvailable() {
        final int connectionStatusCode =
                GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
        if (GooglePlayServicesUtil.isUserRecoverableError(connectionStatusCode)) {
            showGooglePlayServicesAvailabilityErrorDialog(connectionStatusCode);
            return false;
        } else if (connectionStatusCode != ConnectionResult.SUCCESS ) {
            return false;
        }
        return true;
    }

    /**
     * Display an error dialog showing that Google Play Services is missing
     * or out of date.
     * @param connectionStatusCode code describing the presence (or lack of)
     *     Google Play Services on this device.
     */
    void showGooglePlayServicesAvailabilityErrorDialog(
            final int connectionStatusCode) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Dialog dialog = GooglePlayServicesUtil.getErrorDialog(
                        connectionStatusCode,
                        MainActivity.this,
                        REQUEST_GOOGLE_PLAY_SERVICES);
                dialog.show();
            }
        });
    }

}
  • ApiAsyncTask.java
ApiAsyncTask.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package com.example.drivequickstart;

/**
 * Created by dell11 on 2015/9/19.
 */

import android.os.AsyncTask;

import com.google.api.client.googleapis.extensions.android.gms.auth.GooglePlayServicesAvailabilityIOException;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.FileList;


import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * An asynchronous task that handles the Drive API call.
 * Placing the API calls in their own task ensures the UI stays responsive.
 */
public class ApiAsyncTask extends AsyncTask<Void, Void, Void> {
    private MainActivity mActivity;

    /**
     * Constructor.
     * @param activity MainActivity that spawned this task.
     */
    ApiAsyncTask(MainActivity activity) {
        this.mActivity = activity;
    }

    /**
     * Background task to call Drive API.
     * @param params no parameters needed for this task.
     */
    @Override
    protected Void doInBackground(Void... params) {
        try {
            mActivity.clearResultsText();
            mActivity.updateResultsText(getDataFromApi());

        } catch (final GooglePlayServicesAvailabilityIOException availabilityException) {
            mActivity.showGooglePlayServicesAvailabilityErrorDialog(
                    availabilityException.getConnectionStatusCode());

        } catch (UserRecoverableAuthIOException userRecoverableException) {
            mActivity.startActivityForResult(
                    userRecoverableException.getIntent(),
                    MainActivity.REQUEST_AUTHORIZATION);

        } catch (Exception e) {
            mActivity.updateStatus("The following error occurred:\n" +
                    e.getMessage());
        }
        if (mActivity.mProgress.isShowing()) {
            mActivity.mProgress.dismiss();
        }
        return null;
    }

    /**
     * Fetch a list of up to 10 file names and IDs.
     * @return List of Strings describing files, or an empty list if no files
     *         found.
     * @throws IOException
     */
    private List<String> getDataFromApi() throws IOException {
        // Get a list of up to 10 files.
        List<String> fileInfo = new ArrayList<String>();
        FileList result = mActivity.mService.files().list()
                .setMaxResults(20)
                .execute();
        List<File> files = result.getItems();
        if (files != null) {
            for (File file : files) {
                fileInfo.add(String.format("%s (%s)\n",
                        file.getTitle(), file.getId()));
            }
        }
        return fileInfo;
    }
}

然後就可以執行囉,這個app不能執行在虛擬機上,執行的Device必須要有Google Service,執行結果是把你的google drive的前十項名字列出來,這就是個很簡易的Android google drive 應用程式了,這個app就是很單純的去你的google drive拿到前20項檔案名稱然後寫在螢幕上。

如果要其功能完整,在Google Developer中都可以找到些方法,這邊提供上傳檔案的function.

上傳檔案要使用下面這function,出處還是 google developer…..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  /**
   * Insert new file.
   *
   * @param service Drive API service instance.
   * @param title Title of the file to insert, including the extension.
   * @param description Description of the file to insert.
   * @param parentId Optional parent folder's ID.
   * @param mimeType MIME type of the file to insert.
   * @param filename Filename of the file to insert.
   * @return Inserted file metadata if successful, {@code null} otherwise.
   */
  private static File insertFile(Drive service, String title, String description,
      String parentId, String mimeType, String filename) {
    // File's metadata.
    File body = new File();
    body.setTitle(title);
    body.setDescription(description);
    body.setMimeType(mimeType);

    // Set the parent folder.
    if (parentId != null && parentId.length() > 0) {
      body.setParents(
          Arrays.asList(new ParentReference().setId(parentId)));
    }

    // File's content.
    java.io.File fileContent = new java.io.File(filename);
    FileContent mediaContent = new FileContent(mimeType, fileContent);
    try {
      File file = service.files().insert(body, mediaContent).execute();

      // Uncomment the following line to print the File ID.
      // System.out.println("File ID: " + file.getId());

      return file;
    } catch (IOException e) {
      System.out.println("An error occured: " + e);
      return null;
    }
  }

再來實現MediaHttpUploaderProgressListener,就可以知道現在上傳的狀態,function如下:

UploaderProgressListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private class FileUploadProgressListener implements MediaHttpUploaderProgressListener {

    private String mFileUploadedName;

    public FileUploadProgressListener(String fileName) {
        mFileUploadedName = fileName;
    }

    @Override
    public void progressChanged(MediaHttpUploader mediaHttpUploader) throws IOException {
        if (mediaHttpUploader == null) return;
        switch (mediaHttpUploader.getUploadState()) {
            case INITIATION_STARTED:
            //System.out.println("Initiation has started!");
                break;
            case INITIATION_COMPLETE:
            //System.out.println("Initiation is complete!");
                break;
            case MEDIA_IN_PROGRESS:
                double percent = mediaHttpUploader.getProgress() * 100;
                Log.d(TAG, "Upload to GoogleDrive: " + mFileUploadedName + " - " + String.valueOf(percent) + "%");

                break;
            case MEDIA_COMPLETE:

            //System.out.println("Upload is complete!");
        }
    }
}

有了這個Listener之後要把他放到insertFile這個function中,我們改寫一下insertFile(),這邊說明一下insertFile要輸入的值,假設我們要上傳的檔案絕對路徑為 “/sdcard/Download/log.txt” :

  • service :就是google drive quick start 中的mService
1
2
3
4
mService = new com.google.api.services.drive.Drive.Builder(
                transport, jsonFactory, credential)
                .setApplicationName("Drive API Android Quickstart")
                .build();
  • title :上傳之後的檔名,一般就是使用 “log.txt” (對應filename)

  • description :上傳之後的描述,不會影響什麼東西…

  • parentId :資料夾的名稱,如果是根目錄就填 null

  • mimeType :如果你title的extension給對應該就可以直接下null了.

  • filename :這邊應該要下檔案路徑,"/sdcard/Download/log.txt"

大致上就是這樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private static File insertFile(Drive service, String title, String description,
      String parentId, String mimeType, String filename) {
    // File's metadata.
    File body = new File();
    body.setTitle(title);
    body.setDescription(description);
    body.setMimeType(mimeType);

    // Set the parent folder.
    if (parentId != null && parentId.length() > 0) {
      body.setParents(
          Arrays.asList(new ParentReference().setId(parentId)));
    }

    // File's content.
    java.io.File fileContent = new java.io.File(filename);
    FileContent mediaContent = new FileContent(mimeType, fileContent);
    try {
      Drive.Files.Insert insert = service.files().insert(body, mediaContent)
      MediaHttpUploader uploader = insert.getMediaHttpUploader();
      
    uploader.setChunkSize(1024*1024);
    uploader.setProgressListener(new FileUploadProgressListener(filename));
    com.google.api.services.drive.model.File f = insert.execute();


      // Uncomment the following line to print the File ID.
      // System.out.println("File ID: " + file.getId());

      return file;
    } catch (IOException e) {
      System.out.println("An error occured: " + e);
      return null;
    }
  }

Code中的setChunkSize反應出來就是他大概每次分多少bytes上傳,然後每次上傳一個Chunk,Listener就會被Call一次, 應該可以直接在Listener中用UI thread更新progress bar,目前的code只有先把他印出來而已。

我這邊也寫了一個簡單的upload到google drive的app GoogleDriveUpload,結合了Quick start然後再加上Upload按鍵,使用的人記得要把app的名稱改成你的才可以使用喔.

Google Drive api 的部分大致上就是這樣囉,有什麼問題都歡迎討論.

Sep 22nd, 2015

Learn AppRTC/WebRTC

最近在使用WebRTC做些Android上的應用,一開始被Google的這些東西快要搞死,為了方便還是把資料整理一下,丟個教學出來,以便複習跟學習,如果寫的有誤請不吝告知。

都不知道是甚麼的可以先去玩一下 AppRTC Demo
  • WebRTC是Web Real-Time Communication的縮寫,其本意就是可以在Web上實行即時溝通的一個免費且開放的project,其project提供了簡易的api讓開發者便於開發應用.

  • WebRTC的目標為在browser/mobile device/IoT device上開創一個豐富且高品質且統一的溝通協定架構。

一個私有的WebRTC基本上要有
  • Room Server : 主要用來創建跟管理通訊狀況。 (Google原生是使用Nodejs實現,用python驅動)

  • Signaling Server : 主要是管理以及協助終端做點對點的一個角色,負責了控制通訊發起/結束/錯誤的連線消息控制,建立安全連接的關鍵數據,管理雙方在外界所能看到的網路上數據(public ip, port,…) (Google原生使用go language寫的collider)

  • Turn Server : 主要用來協助防火牆穿透的部分,給予Ice candidate等…資訊。 (Google推薦使用coTurn/rfc5766-turn-server)

基本上的架設方法在AppRTC Demo Code可以找到如何架設,如果架設起來應該會長得像他們做出來的AppRTC Demo,不過我在實作的過程中遇到了一些問題就是我的Turn Server架設不起來。

附上找到的一些架設資訊:

AppRTC也有Mobile deviceApplication的版本,在AndroidIOS上都有,官方連結 WebRTC-NativeCode可以進去看看,但是要使用Google官網把整份Source Code弄下來會非常痛苦,而且非常耗時,好在有好心的開發者把已經把相關的library compile (native code…),應該可以讓其他的開發者只要注重其他java的部分,不過當然可以動的地方就比較少,以下是連結:


NOTE:

由於我參考了Etiv的文章後還是有些不懂,所以目前我所有Server都是使用Google的,然後去實現我的Android Application(讓兩隻android device透過WebRTC中的Data Channel來傳輸資料,點對點前的溝通都是透過google架設的Server,目前使用Google的Server都堪用),如果有人整個架設成功還請教一下小弟,至於這個Data Channel怎麼使用因為原生的Source Code中都沒用到,所以沒東西可以參考,在網路上有看到有用的資訊連結在此 Working with data channel in android webrtc application,很多問題通常在Stackflow總是可以找到解答:D

Sep 15th, 2015

Live555 RTSP Server on Android

最近在看 RTSP Server on Android 相關資源,趁現在有空就把它整理整理。

  1. RTSP Server最有名的大概就是 Live555,這套功能還不錯可以支援 .264, .265, .aac, .amr, … 比較重要的是它可以支援 .mkv, .ts (如果Stream這兩種格式都會把Video 跟Audio一同Stream出去, .mkv還會有Subtitle),還有很多格式建議到官網去看一下。

  2. 我找到的另外一個是已經在Android上實現RTSP Server功能的一個Library,叫做libstreaming, 作者叫做fyhertz,他還有另外一個作品叫做spydroid-ipcam, 其功能是把Android手機的Camera跟Microphone透過RTP/UDP發送出去,而libstreaming就是他這套Application的核心library。

(在作者的github網頁中可以找到spydroid-ipcam的source code 跟libstreaming library 還有libstreaming的example1/2/3, 使用者可以使用libstreaming library + libstreaming example1 拼成spydroid-ipcam簡易的樣子)


找好資源後就要自己動工了,如果要讓RTSP Server跑在Android上面並且支援Stream local file的話大概就是兩個選項,在下面一併附上我實作的方法。

  1. 移植Live555到Android上面

    • Live555 Download後解開把source code直接把live下的所有文件放到你想要使用的project下的jni目錄中(需要把project的native support打開並且指定好ndk path)
Console
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Live555project/jni# tree -d
.
├──BasicUsageEnvironment
│   └── include
├── groupsock
│   └── include
├── liveMedia
│   └── include
├── mediaServer
├── proxyServer
├── testProgs
├── UsageEnvironment
│   └── include
└── WindowsAudioInputDevice

檔案太多就不都列出來拉,總之jni下面就是解開live555中live資料夾裡面的東西。

  • 然後要補上Android.mk 跟 一個銜接java 跟 native層的一個.cpp file.

    • Live555的Android.mk在網路上可以找到,連結在此,但是由於他這個版本是2013年的版本,所以使用者使用時必須再加上一些檔案以及mediaServer的部分到LOCAL_SRC_FILE下,再把LOCAL_MODULE的名稱改成 live555, Android.mk一樣是放在jni下。
    • .cpp大概的寫法就是把live555mediaserver.cpp的main function copy把header file寫一寫,然後這隻function name要符合jni的規範讓java層Call的到(ex:Java_com_example_rtspserver_MainActivity_RunRTSPServer…) 然後開始build application,如果你設定都沒有錯應該就會把liblive555.so build出來並且放到project/libs/armeabi/ 下,並且放到.apk中
  • 改libStreaming使之支援Streaming Local file

    • 我改到可以把local file解開然後把其.264部分傳出來,但是這就比較複雜,就不多寫了,主要就是要把URLEncodedUtil那邊看的東西多加local file資訊,這樣Server才會知道Client想要看哪個video,然後要demux該影片,demux這工作可以不用自己來,可以使用ffmpeg或者是Android自帶的api MediaExtractor(Android4.3開始提供),這邊提供如何取出Video的方法:
How to use android MediaExtractor api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
MediaExtractor mExtractor = new MediaExtractor();
ByteBuffer mTransBuffer = ByteBuffer.allocate(1024*1024*2);
private byte[] mTmpBuf = new byte[1024*1024*2];
String path = video_path/abc.mp4;
mExtractor.setDataSource(path);
int numTrack = mExtractor.getTrackCount();
int h264Track = -1;
for(int i=0;i<numTrack;++i)
{
    MediaFormat format = mExtractor.getTrackFormat(i);
    String mime = format.getString(MediaFormat.KEY_MIME);
    if(mime.contains(video) && mime.contains(avc))
    {
        h264Track = i;
    }
}
if(h264Track!=-1)
{
    mExtractor.selectTrack(h264Track);
    while(true)
    {
        int Size = mExtractor.readSampleData(mTransBuffer,0); // read frame to mTransBuffer
        mTransBuffer.get(mTmpBuf);
        mExtractor.advance(); // go next frame
    }
}

以上就是使用android原生的api去demux一個video file,

使用前提是該container format為android support的format,

在readSampleData後把拿到的mTransBuffer裝到mTmpBuf再把mTmpBuf改裝到在H264Packetizer.cpp中的function Send(),

其Send()原本是把Encode好的BitStream取出然後包裝送出,現在就把他替換成我們Demux之後的video source就可以了.

不過用libstreaming library好像有一些問題在,例如Streaming high bitrate的264會有很容易破圖,可能是有更多的細節需要做更改。

Jun 24th, 2015

How to Use Octopress

Something about Octopress


紀錄如何使用github + octopress架個人網站,順便讓自己熟悉一下Markdown語法。

  • 如果是第一次聽到git的人可能要先去熟悉一下git的操作方法,可以到CodeSchool學一下基本的操作方法。
  • octopress的介紹可以看看高手的介紹
  • 關於markdown的好處跟介紹可以參考此連結
  • 環境似乎是在 Mac or Ubuntu 會相對比 Windows方便

Outline :

1. 創建github repository

2. 架設Octopress環境

3. 使用Octopress 發文/刪文

4. 更換Octopress theme

5. 如何在別台電腦上clone你做好的octopress環境

6. 編寫Markdown好用軟體


1. 創建github repository (前提是你已經有github帳號,如果沒有就去註冊)

這邊介紹我使用的方法,登入github之後選擇 + New repository,並且在Repository name的地方填上 username.github.io,之後你就會生出一個叫做username.github.io的Repository,這個就是以後作為你的github page使用。

2. 架設Octopress環境

如何架設Ocotpress環境可以參考官方網站 大概就是

  • 安裝 git
  • 安裝 ruby 1.9.3 ,較建議使用 rvm 安裝
  • 最後一個 Install one of the ExecJS supported JavaScript runtimes. (我沒安裝)

當這些環境都弄好之後,當然就是要把octopress 從git clone下來囉

$ git clone https://github.com/imathis/octopress
$ cd octopress
$ rake install
$ rake setup_github_pages

在這之後他會要求你的github repository, 你可以進到你的github repository右邊有個 SSH clone URL 裡面框框的內容直接複製貼上就可以,如果你第一步跟我是一樣的話,你的這串字應該是 git@github.com:username/username.github.io.git,然後再輸入指令如下:

$ rake generate
$ rake deploy

他就會把目前你的這些東西都推到你的github page上了,這時你可以連到你的github page看看囉(如果沒有生效可能要等一下下)

然後你要把目前目錄下的所有東西push到git上面做個備份

$ git add .
$ git commit -m "github first commit"
$ git push origin source

此時你的github repo會有兩個branch一個是source 一個是master,source是你目前這個octopress資料夾下的所有檔案除了_deploy,而master呢就是你實際的網頁內容(_deploy資料夾裡面的部分)

3. 使用Octopress發文/刪文

發文

$ rake new_post['文章標題']

Octopress會生成個 *.markdown 到你的./source/_posts/ 下面,就我理解這個有點像是原始檔(Markdown Source Code),然後你可以在 *.markdown中用markdown語法寫文章存檔,然後執行

$ rake generate

他會去編譯 *.markdown變成 *.html 放到 _deploy的某個目錄中,然後再執行

$ rake deploy

他會把_deploy新增的或者是刪除的push到github做修改,在po好文章之後你只有把html的部份上傳,可是*.markdown的部分並未上傳做備份,記得再輸入

$ git add .

or

$ git add "新的file path"

then

$ git commit -m "一些敘述"
$ git push
  • 刪文

有了剛剛些概念後應該可以大概知道,使用

$ rake new_post["post_name"]

他其實是幫你把 *.markdown生成到source/_posts/…,然後再透過

$ rake generate

去把html檔生成出來,也就是說如果要刪文的話,你可以到source/_posts/…,去找你想要刪掉的文章

$ rm source/_posts/XXXXXX.markdown

然後在執行

$ rake generate
$ rake deploy

這樣他就會去把在github上面的相關 html給刪除

或者是你可以直接在*.markdown上面有一串字描述這篇文章的屬性的

---
layout: post
title: "How to use Octopress"
date: 2015-06-16 23:30
comments: true
external-url:
categories: octopress
published: true
---

加入pulished屬性,然後把它改成false。

4. 更換Octopress theme

來來來來看這篇你大概就知道怎麼更換Octopress theme囉,連結裡面那篇是一個叫做Slash的主題,非常的簡約風格,就是我現在使用的這個,照著他教學做,你也可以跟我的風格一樣囉

5. 如何在別台電腦上clone你做好的octopress環境

這件事是困擾我最久的,也是我搞最久的,剛好有找到一個很好的教學,前提你架設過octopress然後在github也有紀錄,你是想在另外一條電腦上發文章,你該怎麼複製出一份一樣的環境呢(octopress最基本的ruby跟git是一定要有的)

$ git clone -b source git@github.com:username/username.github.com.git octopress
$ cd octopress
$ git clone git@github.com:username/username.github.com.git _deploy 
$ gem install bundler
$ bundler install

不用再執行

$ rake setup_github_pages

直接可以使用囉。

6. 編寫Markdown好用軟體

  • Miu

    這套僅僅在OSX才有。

  • Mou

    這套是別人品持著Miu的概念,在Windows下開發的Markdown editor。

  • GitBook editor

    這套聽起來就很Git,他是專門拿來寫書用的, 不過應該也可以拿來寫單一頁Markdown文章。

小筆記

在文字前面加上

<a name="something"> 

後面加上

</a> 

可以在文章上面產生一個Internal link, 在想要link到這段的文章給上連結 #something 就可以了,不過在這太放太近了可能感覺不出來,Outline的連結也是這樣做的,可以試試看.

例如:

Go To Case1

Go To Case2

Case1: this is case1

Case2: This is case2

其原始碼為:

Go To [Case1](#case1)

Go To [Case2](#case2)


<a name="case1">Case1</a>:
this is case1

<a name="case2">Case2</a>:
This is case2

參考來源:

Jun 16th, 2015