Skip to content

Commit

Permalink
Use kotlinx-serialization to produce metadata JSON file
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Gunnerson <[email protected]>
  • Loading branch information
chenxiaolong committed Jan 1, 2025
1 parent 08ecb9a commit db0fe8f
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 65 deletions.
81 changes: 41 additions & 40 deletions app/src/main/java/com/chiller3/bcr/RecorderThread.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,24 @@ import com.chiller3.bcr.extension.phoneNumber
import com.chiller3.bcr.extension.toDocumentFile
import com.chiller3.bcr.format.Encoder
import com.chiller3.bcr.format.Format
import com.chiller3.bcr.format.NoParamInfo
import com.chiller3.bcr.format.RangedParamInfo
import com.chiller3.bcr.format.RangedParamType
import com.chiller3.bcr.output.CallMetadata
import com.chiller3.bcr.output.CallMetadataCollector
import com.chiller3.bcr.output.CallMetadataJson
import com.chiller3.bcr.output.DaysRetention
import com.chiller3.bcr.output.FormatJson
import com.chiller3.bcr.output.NoRetention
import com.chiller3.bcr.output.OutputDirUtils
import com.chiller3.bcr.output.OutputFile
import com.chiller3.bcr.output.OutputFilenameGenerator
import com.chiller3.bcr.output.OutputJson
import com.chiller3.bcr.output.OutputPath
import com.chiller3.bcr.output.ParameterType
import com.chiller3.bcr.output.PhoneNumber
import com.chiller3.bcr.output.RecordingJson
import com.chiller3.bcr.output.Retention
import com.chiller3.bcr.rule.RecordRule
import org.json.JSONObject
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.lang.Process
import java.nio.ByteBuffer
import java.time.Duration
Expand Down Expand Up @@ -423,43 +426,37 @@ class RecorderThread(
Log.i(tag, "Writing metadata file")

try {
val formatJson = JSONObject().apply {
put("type", format.name)
put("mime_type_container", format.mimeTypeContainer)
put("mime_type_audio", format.mimeTypeAudio)
put("parameter_type", when (val info = format.paramInfo) {
NoParamInfo -> "none"
is RangedParamInfo -> when (info.type) {
RangedParamType.CompressionLevel -> "compression_level"
RangedParamType.Bitrate -> "bitrate"
}
})
put("parameter", (formatParam ?: format.paramInfo.default).toInt())
}
val recordingJson = if (recordingInfo != null) {
JSONObject().apply {
put("frames_total", recordingInfo.framesTotal)
put("frames_encoded", recordingInfo.framesEncoded)
put("sample_rate", recordingInfo.sampleRate)
put("channel_count", recordingInfo.channelCount)
put("duration_secs_total", recordingInfo.durationSecsTotal)
put("duration_secs_encoded", recordingInfo.durationSecsEncoded)
put("buffer_frames", recordingInfo.bufferFrames)
put("buffer_overruns", recordingInfo.bufferOverruns)
put("was_ever_paused", recordingInfo.wasEverPaused)
put("was_ever_holding", recordingInfo.wasEverHolding)
}
} else {
JSONObject.NULL
}
val outputJson = JSONObject().apply {
put("format", formatJson)
put("recording", recordingJson)
}
val metadataJson = callMetadataCollector.callMetadata.toJson(context).apply {
put("output", outputJson)
val formatJson = FormatJson(
type = format.name,
mimeTypeContainer = format.mimeTypeContainer,
mimeTypeAudio = format.mimeTypeAudio,
parameterType = ParameterType.fromParamInfo(format.paramInfo),
parameter = formatParam ?: format.paramInfo.default,
)
val recordingJson = recordingInfo?.let {
RecordingJson(
framesTotal = it.framesTotal,
framesEncoded = it.framesEncoded,
sampleRate = it.sampleRate,
channelCount = it.channelCount,
durationSecsTotal = it.durationSecsTotal,
durationSecsEncoded = it.durationSecsEncoded,
bufferFrames = it.bufferFrames,
bufferOverruns = it.bufferOverruns,
wasEverPaused = it.wasEverPaused,
wasEverHolding = it.wasEverHolding,
)
}
val metadataBytes = metadataJson.toString(4).toByteArray()
val outputJson = OutputJson(
format = formatJson,
recording = recordingJson,
)
val metadataJson = CallMetadataJson(
context,
callMetadataCollector.callMetadata,
outputJson,
)
val metadataBytes = JSON_FORMAT.encodeToString(metadataJson).toByteArray()

// Always create in the default directory and then move to ensure that we don't race
// with the direct boot file migration process.
Expand Down Expand Up @@ -741,6 +738,10 @@ class RecorderThread(

const val MIME_LOGCAT = "text/plain"
const val MIME_METADATA = "application/json"

private val JSON_FORMAT = Json {
prettyPrint = true
}
}

private data class RecordingInfo(
Expand Down
141 changes: 116 additions & 25 deletions app/src/main/java/com/chiller3/bcr/output/CallMetadata.kt
Original file line number Diff line number Diff line change
@@ -1,41 +1,54 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/

package com.chiller3.bcr.output

import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import com.chiller3.bcr.format.FormatParamInfo
import com.chiller3.bcr.format.NoParamInfo
import com.chiller3.bcr.format.RangedParamInfo
import com.chiller3.bcr.format.RangedParamType
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter

@Serializable
enum class CallDirection {
@SerialName("in")
IN,
@SerialName("out")
OUT,
@SerialName("conference")
CONFERENCE,
;

override fun toString(): String = when (this) {
IN -> "in"
OUT -> "out"
CONFERENCE -> "conference"
}
}

data class CallPartyDetails(
val phoneNumber: PhoneNumber?,
val callerName: String?,
val contactName: String?,
)

@Serializable
data class CallPartyDetailsJson(
@SerialName("phone_number")
val phoneNumber: String?,
@SerialName("phone_number_formatted")
val phoneNumberFormatted: String?,
@SerialName("caller_name")
val callerName: String?,
@SerialName("contact_name")
val contactName: String?,
) {
fun toJson(context: Context) = JSONObject().apply {
put("phone_number", phoneNumber?.toString() ?: JSONObject.NULL)
put("phone_number_formatted",
phoneNumber?.format(context, PhoneNumber.Format.COUNTRY_SPECIFIC) ?: JSONObject.NULL)
put("caller_name", callerName ?: JSONObject.NULL)
put("contact_name", contactName ?: JSONObject.NULL)
}
constructor(context: Context, details: CallPartyDetails) : this(
phoneNumber = details.phoneNumber?.toString(),
phoneNumberFormatted = details.phoneNumber
?.format(context, PhoneNumber.Format.COUNTRY_SPECIFIC),
callerName = details.callerName,
contactName = details.contactName,
)
}

data class CallMetadata(
Expand All @@ -45,13 +58,91 @@ data class CallMetadata(
val simSlot: Int?,
val callLogName: String?,
val calls: List<CallPartyDetails>,
)

@Serializable
data class CallMetadataJson(
@SerialName("timestamp_unix_ms")
val timestampUnixMs: Long,
val timestamp: String,
val direction: CallDirection?,
@SerialName("sim_slot")
val simSlot: Int?,
@SerialName("call_log_name")
val callLogName: String?,
val calls: List<CallPartyDetailsJson>,
val output: OutputJson,
) {
fun toJson(context: Context) = JSONObject().apply {
put("timestamp_unix_ms", timestamp.toInstant().toEpochMilli())
put("timestamp", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(timestamp))
put("direction", direction?.toString() ?: JSONObject.NULL)
put("sim_slot", simSlot ?: JSONObject.NULL)
put("call_log_name", callLogName ?: JSONObject.NULL)
put("calls", JSONArray(calls.map { it.toJson(context) }))
constructor(context: Context, metadata: CallMetadata, output: OutputJson) : this(
timestampUnixMs = metadata.timestamp.toInstant().toEpochMilli(),
timestamp = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(metadata.timestamp),
direction = metadata.direction,
simSlot = metadata.simSlot,
callLogName = metadata.callLogName,
calls = metadata.calls.map { CallPartyDetailsJson(context, it) },
output = output,
)
}

@Serializable
enum class ParameterType {
@SerialName("none")
NONE,
@SerialName("compression_level")
COMPRESSION_LEVEL,
@SerialName("bitrate")
BITRATE,
;

companion object {
fun fromParamInfo(info: FormatParamInfo): ParameterType = when (info) {
NoParamInfo -> NONE
is RangedParamInfo -> when (info.type) {
RangedParamType.CompressionLevel -> COMPRESSION_LEVEL
RangedParamType.Bitrate -> BITRATE
}
}
}
}
}

@Serializable
data class FormatJson(
val type: String,
@SerialName("mime_type_container")
val mimeTypeContainer: String,
@SerialName("mime_type_audio")
val mimeTypeAudio: String,
@SerialName("parameter_type")
val parameterType: ParameterType,
val parameter: UInt,
)

@Serializable
data class RecordingJson(
@SerialName("frames_total")
val framesTotal: Long,
@SerialName("frames_encoded")
val framesEncoded: Long,
@SerialName("sample_rate")
val sampleRate: Int,
@SerialName("channel_count")
val channelCount: Int,
@SerialName("duration_secs_total")
val durationSecsTotal: Double,
@SerialName("duration_secs_encoded")
val durationSecsEncoded: Double,
@SerialName("buffer_frames")
val bufferFrames: Long,
@SerialName("buffer_overruns")
val bufferOverruns: Int,
@SerialName("was_ever_paused")
val wasEverPaused: Boolean,
@SerialName("was_ever_holding")
val wasEverHolding: Boolean,
)

@Serializable
data class OutputJson(
val format: FormatJson,
val recording: RecordingJson?,
)

0 comments on commit db0fe8f

Please sign in to comment.