Work on call recording

This commit is contained in:
Juha Heinanen
2023-03-09 08:12:29 +02:00
parent 205b129b4f
commit de1f52b9ce
7 changed files with 199 additions and 4 deletions

View File

@ -60,6 +60,10 @@ add_library(lib_zrtpcppcore STATIC IMPORTED)
set_target_properties(lib_zrtpcppcore PROPERTIES IMPORTED_LOCATION set_target_properties(lib_zrtpcppcore PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/gzrtp/lib/${ANDROID_ABI}/libzrtpcppcore.a) ${distribution_DIR}/gzrtp/lib/${ANDROID_ABI}/libzrtpcppcore.a)
add_library(lib_sndfile STATIC IMPORTED)
set_target_properties(lib_sndfile PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/sndfile/lib/${ANDROID_ABI}/libsndfile.a)
add_library(lib_baresip STATIC IMPORTED) add_library(lib_baresip STATIC IMPORTED)
set_target_properties(lib_baresip PROPERTIES IMPORTED_LOCATION set_target_properties(lib_baresip PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/baresip/lib/${ANDROID_ABI}/libbaresip.a) ${distribution_DIR}/baresip/lib/${ANDROID_ABI}/libbaresip.a)
@ -90,5 +94,6 @@ target_link_libraries(baresip
lib_amrwbenc lib_amrwbenc
lib_webrtc lib_webrtc
lib_zrtpcppcore lib_zrtpcppcore
lib_sndfile
z z
log) log)

View File

@ -147,7 +147,8 @@ static void ua_event_handler(struct ua *ua, enum ua_event ev,
const char *tone; const char *tone;
char event_buf[256]; char event_buf[256];
enum sdp_dir ardir; enum sdp_dir ardir;
int len; int len, err;
struct pl module, module_event, data;
LOGD("ua event (%s) %s\n", uag_event_str(ev), prm); LOGD("ua event (%s) %s\n", uag_event_str(ev), prm);
@ -225,6 +226,15 @@ static void ua_event_handler(struct ua *ua, enum ua_event ev,
case UA_EVENT_MWI_NOTIFY: case UA_EVENT_MWI_NOTIFY:
len = re_snprintf(event_buf, sizeof event_buf, "mwi notify,%s", prm); len = re_snprintf(event_buf, sizeof event_buf, "mwi notify,%s", prm);
break; break;
case UA_EVENT_MODULE:
err = re_regex(prm, strlen(prm), "[^,]*,[^,]*,[~]*",
&module, &module_event, &data);
if (err)
return;
if (!pl_strcmp(&module_event, "dump")) {
len = re_snprintf(event_buf, sizeof event_buf, "sndfile dump,%r", &data);
break;
}
default: default:
return; return;
} }
@ -1088,7 +1098,7 @@ Java_com_tutpro_baresip_Api_ua_1answer(JNIEnv *env, jobject thiz, jlong ua, jlon
re_thread_leave(); re_thread_leave();
} }
JNIEXPORT jint JNICALL JNIEXPORT void JNICALL
Java_com_tutpro_baresip_Api_ua_1add_1custom_1header(JNIEnv *env, jobject thiz, jlong ua, Java_com_tutpro_baresip_Api_ua_1add_1custom_1header(JNIEnv *env, jobject thiz, jlong ua,
jstring jname, jstring jbody) { jstring jname, jstring jbody) {
const char *name = (*env)->GetStringUTFChars(env, jname, 0); const char *name = (*env)->GetStringUTFChars(env, jname, 0);
@ -1104,7 +1114,6 @@ Java_com_tutpro_baresip_Api_ua_1add_1custom_1header(JNIEnv *env, jobject thiz, j
LOGW("adding custom header to ua %ld failed with error %d\n", (long)ua, err); LOGW("adding custom header to ua %ld failed with error %d\n", (long)ua, err);
(*env)->ReleaseStringUTFChars(env, jname, name); (*env)->ReleaseStringUTFChars(env, jname, name);
(*env)->ReleaseStringUTFChars(env, jbody, body); (*env)->ReleaseStringUTFChars(env, jbody, body);
return err;
} }
JNIEXPORT void JNICALL JNIEXPORT void JNICALL

View File

@ -43,6 +43,7 @@ import androidx.media.AudioManagerCompat
import java.io.File import java.io.File
import java.net.InetAddress import java.net.InetAddress
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -384,6 +385,23 @@ class BaresipService: Service() {
if (File(filesDir, "history").exists()) if (File(filesDir, "history").exists())
File(filesDir, "history").renameTo(File(filesDir, "calls")) File(filesDir, "history").renameTo(File(filesDir, "calls"))
if (!File(filesDir, "tmp").exists())
File(filesDir, "tmp").mkdir()
if (!File(filesDir, "recordings").exists())
File(filesDir, "recordings").mkdir()
else {
File(filesDir, "recordings").walk().forEach { f ->
Log.d(TAG, "***** file ${f.name}")
val recording = f.name.substringBeforeLast("-")
if (recording.startsWith("dump")) {
val time = recording.takeLast(19)
if (!recordings.containsKey(time))
recordings[time] = filesDir.absolutePath + "/recordings/" + recording
}
}
Log.d(TAG, "Recordings $recordings")
}
if (contactsMode != "android") if (contactsMode != "android")
Contact.restoreBaresipContacts() Contact.restoreBaresipContacts()
@ -573,6 +591,11 @@ class BaresipService: Service() {
return return
} }
if (ev[0] == "sndfile dump") {
Log.d(TAG, "Got sndfile dump ${ev[1]}")
return
}
val ua = UserAgent.ofUap(uap) val ua = UserAgent.ofUap(uap)
if (ua == null) { if (ua == null) {
Log.w(TAG, "uaEvent $event did not find ua $uap") Log.w(TAG, "uaEvent $event did not find ua $uap")
@ -877,6 +900,23 @@ class BaresipService: Service() {
NewCallHistory.add(history) NewCallHistory.add(history)
NewCallHistory.save() NewCallHistory.save()
ua.account.missedCalls = ua.account.missedCalls || missed ua.account.missedCalls = ua.account.missedCalls || missed
if (isRecOn && call.startTime != null) {
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")
val startTime = call.startTime!!
var time = format.format(startTime.time).toString()
var recording = filesDir.absolutePath + "/recordings/" +
"dump-$aor=>${call.peerUri}-$time"
if (File("$recording-dec.wav").exists()) {
recordings[time] = recording
} else {
startTime.add(Calendar.SECOND, 1)
time = format.format(startTime.time).toString()
recording = filesDir.absolutePath + "/recordings/" +
"dump-$aor=>${call.peerUri}-$time"
recordings[time] = recording
}
}
} }
if (!Utils.isVisible()) { if (!Utils.isVisible()) {
if (missed) { if (missed) {
@ -1496,6 +1536,7 @@ class BaresipService: Service() {
var callActionUri = "" var callActionUri = ""
var isMainVisible = false var isMainVisible = false
var isMicMuted = false var isMicMuted = false
var isRecOn = false
val uas = ArrayList<UserAgent>() val uas = ArrayList<UserAgent>()
val calls = ArrayList<Call>() val calls = ArrayList<Call>()
@ -1514,6 +1555,7 @@ class BaresipService: Service() {
var dnsServers = listOf<InetAddress>() var dnsServers = listOf<InetAddress>()
val serviceEvent = MutableLiveData<Event<Long>>() val serviceEvent = MutableLiveData<Event<Long>>()
val serviceEvents = mutableListOf<ServiceEvent>() val serviceEvents = mutableListOf<ServiceEvent>()
val recordings: MutableMap<String, String> = mutableMapOf()
private var audioFocusRequest: AudioFocusRequestCompat? = null private var audioFocusRequest: AudioFocusRequestCompat? = null
private var btAdapter: BluetoothAdapter? = null private var btAdapter: BluetoothAdapter? = null

View File

@ -1,6 +1,8 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Typeface
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -9,6 +11,7 @@ import android.widget.ArrayAdapter
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -23,6 +26,7 @@ class CallDetailsAdapter(private val ctx: Context, private val rows: ArrayList<C
val durationView = view?.findViewById(R.id.duration) as TextView val durationView = view?.findViewById(R.id.duration) as TextView
} }
@SuppressLint("SimpleDateFormat")
override fun getView(position: Int, view: View?, parent: ViewGroup): View { override fun getView(position: Int, view: View?, parent: ViewGroup): View {
val viewHolder: ViewHolder val viewHolder: ViewHolder
@ -59,7 +63,7 @@ class CallDetailsAdapter(private val ctx: Context, private val rows: ArrayList<C
viewHolder.durationView.text = "" viewHolder.durationView.text = ""
} else { } else {
val startText = if (DateUtils.isToday(startTime.timeInMillis)) { val startText = if (DateUtils.isToday(startTime.timeInMillis)) {
val fmt = DateFormat.getTimeInstance(DateFormat.LONG) val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
ctx.getString(R.string.today) + " " + fmt.format(startTime.time) ctx.getString(R.string.today) + " " + fmt.format(startTime.time)
} else { } else {
val fmt = DateFormat.getDateTimeInstance() val fmt = DateFormat.getDateTimeInstance()
@ -68,6 +72,14 @@ class CallDetailsAdapter(private val ctx: Context, private val rows: ArrayList<C
viewHolder.timeView.text = startText viewHolder.timeView.text = startText
val duration = (stopTime.time.time - startTime.time.time) / 1000 val duration = (stopTime.time.time - startTime.time.time) / 1000
viewHolder.durationView.text = DateUtils.formatElapsedTime(duration) viewHolder.durationView.text = DateUtils.formatElapsedTime(duration)
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")
val time = format.format(startTime.time).toString()
if (BaresipService.recordings.containsKey(time)) {
viewHolder.durationView.typeface = Typeface.DEFAULT_BOLD
viewHolder.durationView.setOnClickListener {
Utils.playRecording(ctx, BaresipService.recordings[time]!!)
}
}
} }
} }

View File

@ -115,6 +115,8 @@ object Config {
if (!config.contains("dtls_srtp_use_ec")) if (!config.contains("dtls_srtp_use_ec"))
config = "${config}dtls_srtp_use_ec prime256v1\n" config = "${config}dtls_srtp_use_ec prime256v1\n"
replaceVariable("snd_path", "${BaresipService.filesPath}/recordings")
Utils.putFileContents(configPath, config.toByteArray()) Utils.putFileContents(configPath, config.toByteArray())
BaresipService.isConfigInitialized = true BaresipService.isConfigInitialized = true
Log.i(TAG, "Initialized config to '$config'") Log.i(TAG, "Initialized config to '$config'")

View File

@ -76,6 +76,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var kgm: KeyguardManager private lateinit var kgm: KeyguardManager
private lateinit var screenEventReceiver: BroadcastReceiver private lateinit var screenEventReceiver: BroadcastReceiver
private lateinit var serviceEventObserver: Observer<Event<Long>> private lateinit var serviceEventObserver: Observer<Event<Long>>
private var recIcon: MenuItem? = null
private var micIcon: MenuItem? = null private var micIcon: MenuItem? = null
private var speakerIcon: MenuItem? = null private var speakerIcon: MenuItem? = null
private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var swipeRefresh: SwipeRefreshLayout
@ -1258,6 +1259,13 @@ class MainActivity : AppCompatActivity() {
menuInflater.inflate(R.menu.main_menu, menu) menuInflater.inflate(R.menu.main_menu, menu)
menuInflater.inflate(R.menu.rec_icon, menu)
recIcon = menu.findItem(R.id.recIcon)
if (BaresipService.isRecOn)
recIcon!!.setIcon(R.drawable.rec_on)
else
recIcon!!.setIcon(R.drawable.rec_off)
menuInflater.inflate(R.menu.mic_icon, menu) menuInflater.inflate(R.menu.mic_icon, menu)
micIcon = menu.findItem(R.id.micIcon) micIcon = menu.findItem(R.id.micIcon)
if (BaresipService.isMicMuted) if (BaresipService.isMicMuted)
@ -1279,6 +1287,19 @@ class MainActivity : AppCompatActivity() {
when (item.itemId) { when (item.itemId) {
R.id.recIcon -> {
if (Call.call("connected") == null) {
BaresipService.isRecOn = !BaresipService.isRecOn
if (BaresipService.isRecOn) {
item.setIcon(R.drawable.rec_on)
Api.module_load("sndfile")
} else {
item.setIcon(R.drawable.rec_off)
Api.module_unload("sndfile")
}
}
}
R.id.micIcon -> { R.id.micIcon -> {
if (Call.call("connected") != null) { if (Call.call("connected") != null) {
BaresipService.isMicMuted = !BaresipService.isMicMuted BaresipService.isMicMuted = !BaresipService.isMicMuted

View File

@ -10,6 +10,7 @@ import android.graphics.Bitmap.createScaledBitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.media.AudioAttributes
import android.net.Uri import android.net.Uri
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.provider.DocumentsContract import android.provider.DocumentsContract
@ -24,10 +25,12 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.media.AudioDeviceInfo import android.media.AudioDeviceInfo
import android.media.AudioManager import android.media.AudioManager
import android.media.MediaPlayer
import android.os.* import android.os.*
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
@ -992,4 +995,105 @@ object Utils {
} }
} }
fun playRecording(ctx: Context, recording: String) {
Log.d(TAG, "Playing recording $recording")
val decPlayer = MediaPlayer()
decPlayer.apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setOnPreparedListener {
val encPlayer = MediaPlayer()
encPlayer.apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setOnPreparedListener {
it.start()
decPlayer.start()
Log.d(TAG, "Started players")
}
setOnCompletionListener {
Log.d(TAG, "Stopping encPlayer")
it.stop()
it.release()
}
try {
val file = "$recording-enc.wav"
val encFile = File(file).copyTo(File(BaresipService.filesPath + "/tmp/encode.wav"), true)
val encUri = encFile.toUri()
Log.d(TAG, "Setting data source $encUri")
setDataSource(ctx, encUri)
prepareAsync()
} catch (e: IllegalArgumentException) {
Log.e(TAG, "encPlayer IllegalArgumentException: $e")
} catch (e: IOException) {
Log.e(TAG, "encPlayer IOException: $e")
} catch (e: Exception) {
Log.e(TAG, "encPlayer Exception: $e")
}
}
}
setOnCompletionListener {
Log.d(TAG, "Stopping decPlayer")
it.stop()
it.release()
}
try {
val file = "$recording-dec.wav"
val decFile = File(file).copyTo(File(BaresipService.filesPath + "/tmp/decode.wav"), true)
val decUri = decFile.toUri()
Log.d(TAG, "Setting data source $decUri")
setDataSource(ctx, decUri)
prepareAsync()
} catch (e: IllegalArgumentException) {
Log.e(TAG, "decPlayer IllegalArgumentException: $e")
} catch (e: IOException) {
Log.e(TAG, "decPlayer IOException: $e")
} catch (e: Exception) {
Log.e(TAG, "decPlayer Exception: $e")
}
}
}
@Suppress("unused")
fun playFile(ctx: Context, path: String) {
Log.d(TAG, "Playing file $path")
MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setOnPreparedListener {
Log.d(TAG, "Starting MediaPlayer")
it.start()
Log.d(TAG, "MediaPlayer started")
}
setOnCompletionListener {
Log.d(TAG, "Stopping MediaPlayer")
it.stop()
it.release()
}
try {
Log.d(TAG, "Preparing $path")
setDataSource(ctx, path.toUri())
prepareAsync()
} catch (e: IllegalArgumentException) {
Log.e(TAG, "MediaPlayer IllegalArgumentException: ${e.printStackTrace()}")
} catch (e: IOException) {
Log.e(TAG, "MediaPlayer IOException: ${e.printStackTrace()}")
} catch (e: Exception) {
Log.e(TAG, "MediaPlayer Exception: ${e.printStackTrace()}")
}
}
}
} }