近期應公司要求,因為是一個銷售型公司,所以銷售人員需要通話錄音的一個需求,所以在老板的要求下,實現(xiàn)了這個項目。
1、app實現(xiàn)雙向錄音;
2、上傳錄音文件到node.js服務器,將音頻文件存到oss中(因為我的服務器用的是阿里云的oss存儲文件);
3、在數(shù)據(jù)庫中存儲錄音的訪問地址;
4、在公司內(nèi)部的oa系統(tǒng)中顯示可播放該錄音。
android實現(xiàn)錄音,我用的是MediaRecorder,剛開始錄制的音頻格式是.3gp的。之后采用了segmentFault上面一個大神的說法,用MediaRecorder將錄音錄制成mp4格式,用aac編碼,只需要把后綴名改成mp3格式的就可以了。實現(xiàn)思路如下:
注冊一個服務為PhoneListenServer繼承自Server。這個server類因為是服務,所以可以監(jiān)聽通話錄音的狀態(tài),當有電話打進來時就可以監(jiān)聽到,監(jiān)聽到之后就可以實現(xiàn)MediaRecorder錄音機的錄音,至于電話撥出的監(jiān)聽,是注冊一個廣播接收器MyPhoneStateReceiver myPhoneStateReceiver,registerReceiver(myPhoneStateReceiver, intentFilter)。文件上傳我采用的是OkHttp3的文件上傳,因為我的錄音數(shù)據(jù)放在sd卡新建的兩個文件夾中recorder_callMonitor_from和recorder_callMonitor_outgoingcall。所以上傳文件的時候獲取文件,并將當前用戶的信息和來電去電信息一起上傳到服務器進行處理。
具體實現(xiàn)代碼我貼出來,因為注釋特別詳細,我就大致講解一下就好了。不多啰嗦。
一、android端。

getOutgoingCall()這個函數(shù)是監(jiān)聽去電廣播




MyListener這個類繼承自PhoneStateListener,主要用來監(jiān)聽通話狀態(tài)并且實現(xiàn)錄音和錄音文件的存儲。
下面是文件上傳的功能,是在線程中完成的
final String[] flag = new String[1];
private final class UploadTask implements Runnable {
@Override
public void run() {
/**
* 對傳輸?shù)臄?shù)據(jù)進行封裝
*/
final String filename; // 文件名(當前打電話的電話通話對方的號碼)
final String filePath; // 文件路徑,錄制的音頻所在的sd卡路徑
String path = Environment.getExternalStorageDirectory().getPath();
final String outPath = path + "/recorder_callMonitor_outgoingcall" + "/" + inComingNumber + ".mp3";
final String fromPath = path + "/recorder_callMonitor_from" + "/" + inComingNumber + ".mp3";
if (isExist(outPath)) {
filePath = outPath;
filename = getFileName(outPath);
flag[0] = "0";
} else {
filePath = fromPath;
filename = getFileName(fromPath);
flag[0] = "1";
}
File file = new File(filePath);
if (!file.exists()) {
L.e(file.getAbsolutePath() + " not exist!");
return;
}
// 數(shù)據(jù)封裝完畢
// 1 拿到okHttpClient對象
final OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(5000, TimeUnit.MILLISECONDS)
.readTimeout(5000,TimeUnit.MILLISECONDS)
.build();
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("phoneCustomer",filename)
.addFormDataPart("phoneUser",getLocalName())
.addFormDataPart("type",flag[0])
.addFormDataPart("file",filename,RequestBody.create(MediaType.parse("audio/mp3"),file))
.build();
// 2 構造Request
Request.Builder builder = new Request.Builder();
Request request = builder.url("https://oa.100xuetang.com/android/audioFile/android_uploadAudio")
.post(requestBody)
.build();
// 3 將Request封裝為Call
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
L.e("onFailure: " + e.getMessage());
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
L.e("onResponse:");
String res = response.body().string();
L.e(res);
Log.i("result", "success to save audio to oss & mysql");
successHander();
}
});
}
}
先對需要上傳的數(shù)據(jù)進行封裝,然后調(diào)用OkHttpClient對象。順便將一下OkHttp3這個網(wǎng)絡框架的使用。
1、先聲明一個OkHttpClient對象,可以在這個時候設置超時時間。記得別忘記.build();
2、對數(shù)據(jù)封裝到RequestBody這個對象中。

就像這樣,別忘記.build();
3、構造Request對象,主要有兩個,一個是url,一個是post(requestBody);
4、將Request封裝成Call,這個是必須的一步,call.execute()同步執(zhí)行上傳操作,call.enqueue(new Callback(){...})是將這個網(wǎng)絡請求放到隊列中等待執(zhí)行,是異步,這個里面需要強調(diào)的一點是回調(diào)函數(shù)里面的onResponse()在另一個線程中,不是主線程。
下面是幾個輔助函數(shù),我都列出來吧。既然造輪子就得讓最不會用的人也會使用嘛
public void successHander(){
// 第一步:刪除本地存儲下來的音頻文件,防止占用內(nèi)存空間過大。
deleteDir(Environment.getExternalStorageDirectory().getPath() + "/recorder_callMonitor_outgoingcall");
deleteDir(Environment.getExternalStorageDirectory().getPath() + "/recorder_callMonitor_from");
// 第二步:flag置為未知,當有電話來或者有電話撥出時候再重新賦值。
flag[0] = "-1";
// 第三步: inComingNumber 和 callNumber 置為空,防止誤命名錄音文件
inComingNumber = "";
callNum = "";
phoneNumber = "";
}
@Override
public void onDestroy() {
super.onDestroy();
// 取消電話的監(jiān)聽,采取線程守護的方法,當一個服務關閉后,開啟另外一個服務,除非你很快把兩個服務同時關閉才能完成
Intent i = new Intent(this,TelProtectService.class);
startService(i);
listener = null;
System.out.println("關閉了服務");
}
/**
* 提取文件名
* @param pathandname 文件路徑
* @return
*/
public String getFileName(String pathandname){
int start=pathandname.lastIndexOf("/");
int end=pathandname.lastIndexOf(".");
if(start!=-1 && end!=-1){
return pathandname.substring(start+1,end);
}else{
return null;
}
}
/**
* 獲得保存在本地的用戶名
*/
public String getLocalName() {
//獲取SharedPreferences對象,使用自定義類的方法來獲取對象
SharedPreferencesUtils helper = new SharedPreferencesUtils(this, "setting");
String name = helper.getString("name");
return name;
}
/**
* 判斷文件夾是否存在
* @param path 文件夾路徑
*/
public boolean isExist(String path) {
File file = new File(path);
//判斷文件夾是否存在,如果不存在則創(chuàng)建文件夾
if (!file.exists()) {
return false; // 不存在
} else {
return true; // 存在
}
}
/**
* 刪除文件夾和文件夾里面的文件
* @param pPath
*/
private void deleteDir(final String pPath) {
File dir = new File(pPath);
deleteDirWihtFile(dir);
}
private void deleteDirWihtFile(File dir) {
if (dir == null || !dir.exists() || !dir.isDirectory())
return;
for (File file : dir.listFiles()) {
if (file.isFile())
file.delete(); // 刪除所有文件
else if (file.isDirectory())
deleteDirWihtFile(file); // 遞規(guī)的方式刪除文件夾
}
dir.delete();// 刪除目錄本身
}
線程守護的類:
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.util.Log;
/**
-
保護監(jiān)聽服務Service
*/
public class TelProtectService extends Service{@Override
public void onCreate() {
Intent i = new Intent(this, PhoneStateListener.class);
startService(i);
Log.i("TelProtectService", "TelProtectService.守護進程");
super.onCreate();
}@Override
public IBinder onBind(Intent intent) {
return null;
}
}
二、node.js服務器端
首先是在Controller文件夾下audioFile.js下的android_uploadAudio這個路由,express框架這個是用nodeJs人最熟悉的了。multer這個框架require一下,因為是涉及到file。

我的函數(shù)封裝在Models下的android/upload.js里面。


這兒我不多做介紹了,邏輯上就是先從數(shù)據(jù)庫拿到傳過來的兩個手機號對應的課程顧問的id和顧客的id,然后先將文件讀出來,再命名,存儲到oss中,同時存儲到數(shù)據(jù)庫一條記錄,然后callbackOk()。