近期應(yīng)公司要求,因為是一個銷售型公司捷雕,所以銷售人員需要通話錄音的一個需求评肆,所以在老板的要求下,實現(xiàn)了這個項目非区。
1瓜挽、app實現(xiàn)雙向錄音;
2征绸、上傳錄音文件到node.js服務(wù)器久橙,將音頻文件存到oss中(因為我的服務(wù)器用的是阿里云的oss存儲文件);
3管怠、在數(shù)據(jù)庫中存儲錄音的訪問地址淆衷;
4、在公司內(nèi)部的oa系統(tǒng)中顯示可播放該錄音渤弛。
android實現(xiàn)錄音祝拯,我用的是MediaRecorder,剛開始錄制的音頻格式是.3gp的。之后采用了segmentFault上面一個大神的說法佳头,用MediaRecorder將錄音錄制成mp4格式鹰贵,用aac編碼,只需要把后綴名改成mp3格式的就可以了康嘉。實現(xiàn)思路如下:
注冊一個服務(wù)為PhoneListenServer繼承自Server碉输。這個server類因為是服務(wù),所以可以監(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侨赡。所以上傳文件的時候獲取文件,并將當前用戶的信息和來電去電信息一起上傳到服務(wù)器進行處理纱控。
具體實現(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 構(gòu)造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)絡(luò)框架的使用。
1藐不、先聲明一個OkHttpClient對象匀哄,可以在這個時候設(shè)置超時時間。記得別忘記.build();
2雏蛮、對數(shù)據(jù)封裝到RequestBody這個對象中涎嚼。
就像這樣,別忘記.build()挑秉;
3法梯、構(gòu)造Request對象,主要有兩個犀概,一個是url立哑,一個是post(requestBody)夜惭;
4、將Request封裝成Call刁憋,這個是必須的一步滥嘴,call.execute()同步執(zhí)行上傳操作,call.enqueue(new Callback(){...})是將這個網(wǎng)絡(luò)請求放到隊列中等待執(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)聽,采取線程守護的方法,當一個服務(wù)關(guān)閉后尤莺,開啟另外一個服務(wù)旅敷,除非你很快把兩個服務(wù)同時關(guān)閉才能完成
Intent i = new Intent(this,TelProtectService.class);
startService(i);
listener = null;
System.out.println("關(guān)閉了服務(wù)");
}
/**
* 提取文件名
* @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)聽服務(wù)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服務(wù)器端
首先是在Controller文件夾下audioFile.js下的android_uploadAudio這個路由媳谁,express框架這個是用nodeJs人最熟悉的了。multer這個框架require一下友酱,因為是涉及到file晴音。
我的函數(shù)封裝在Models下的android/upload.js里面。
這兒我不多做介紹了缔杉,邏輯上就是先從數(shù)據(jù)庫拿到傳過來的兩個手機號對應(yīng)的課程顧問的id和顧客的id锤躁,然后先將文件讀出來,再命名或详,存儲到oss中进苍,同時存儲到數(shù)據(jù)庫一條記錄,然后callbackOk()鸭叙。