From 69c8be30b5ec633e15e3a297fed11753a87035c4 Mon Sep 17 00:00:00 2001 From: Danail Kirov Date: Mon, 13 Nov 2023 13:46:37 -0800 Subject: [PATCH 1/7] Colibri msg initiated pcap capture --- jitsi-media-transform/pom.xml | 6 + .../main/kotlin/org/jitsi/nlj/RtpReceiver.kt | 2 + .../kotlin/org/jitsi/nlj/RtpReceiverImpl.kt | 8 + .../main/kotlin/org/jitsi/nlj/RtpSender.kt | 1 + .../kotlin/org/jitsi/nlj/RtpSenderImpl.kt | 8 + .../main/kotlin/org/jitsi/nlj/Transceiver.kt | 5 + .../transform/node/CompliancePcapWriter.kt | 264 ++++++++++++++++++ .../src/main/resources/reference.conf | 8 + .../colibri2/Colibri2ConferenceHandler.kt | 11 + 9 files changed, 313 insertions(+) create mode 100644 jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt diff --git a/jitsi-media-transform/pom.xml b/jitsi-media-transform/pom.xml index 1e54e4d958..2a46636c36 100644 --- a/jitsi-media-transform/pom.xml +++ b/jitsi-media-transform/pom.xml @@ -135,6 +135,12 @@ 2.3.1 test + + com.google.code.gson + gson + 2.9.0 + compile + diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiver.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiver.kt index 5a55659b8c..067525b51b 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiver.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiver.kt @@ -63,6 +63,8 @@ abstract class RtpReceiver : abstract fun forceMuteAudio(shouldMute: Boolean) abstract fun forceMuteVideo(shouldMute: Boolean) + + abstract fun setComplianceRecording(comRec: String?, contextId: String?) } interface RtpReceiverEventHandler { diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiverImpl.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiverImpl.kt index 8f749fef38..0f1d4178b3 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiverImpl.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiverImpl.kt @@ -32,6 +32,7 @@ import org.jitsi.nlj.stats.RtpReceiverStats import org.jitsi.nlj.transform.NodeEventVisitor import org.jitsi.nlj.transform.NodeStatsVisitor import org.jitsi.nlj.transform.NodeTeardownVisitor +import org.jitsi.nlj.transform.node.CompliancePcapWriter import org.jitsi.nlj.transform.node.ConsumerNode import org.jitsi.nlj.transform.node.Node import org.jitsi.nlj.transform.node.PacketLossConfig @@ -140,6 +141,7 @@ class RtpReceiverImpl @JvmOverloads constructor( }) } private val toggleablePcapWriter = ToggleablePcapWriter(logger, "$id-rx") + private val compliancePcapWriter = CompliancePcapWriter(logger, streamInformationStore, id, "ne") private val videoBitrateCalculator = VideoBitrateCalculator(parentLogger) private val audioBitrateCalculator = BitrateCalculator("Audio bitrate calculator") @@ -225,6 +227,7 @@ class RtpReceiverImpl @JvmOverloads constructor( // This reads audio levels from packets that use cryptex. TODO: should it go in the Audio path? node(audioLevelReader.postDecryptNode) node(toggleablePcapWriter.newObserverNode()) + node(compliancePcapWriter.newObserverNode()) node(statsTracker) node(PaddingTermination(logger)) demux("Media Type") { @@ -350,9 +353,14 @@ class RtpReceiverImpl @JvmOverloads constructor( NodeTeardownVisitor().visit(inputTreeRoot) incomingPacketQueue.close() toggleablePcapWriter.disable() + compliancePcapWriter.disable() } override fun onRttUpdate(newRttMs: Double) { remoteBandwidthEstimator.onRttUpdate(newRttMs) } + + override fun setComplianceRecording(comRec: String?, contextId: String?) { + compliancePcapWriter.configure(comRec, contextId) + } } diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSender.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSender.kt index 639a4373ab..77de1ba99f 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSender.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSender.kt @@ -47,6 +47,7 @@ abstract class RtpSender : abstract fun setFeature(feature: Features, enabled: Boolean) abstract fun isFeatureEnabled(feature: Features): Boolean abstract fun tearDown() + abstract fun setComplianceRecording(comRec: String?, contextId: String?) abstract val bandwidthEstimator: BandwidthEstimator } diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSenderImpl.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSenderImpl.kt index b568116e5f..19e067513d 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSenderImpl.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSenderImpl.kt @@ -32,6 +32,7 @@ import org.jitsi.nlj.transform.NodeEventVisitor import org.jitsi.nlj.transform.NodeStatsVisitor import org.jitsi.nlj.transform.NodeTeardownVisitor import org.jitsi.nlj.transform.node.AudioRedHandler +import org.jitsi.nlj.transform.node.CompliancePcapWriter import org.jitsi.nlj.transform.node.ConsumerNode import org.jitsi.nlj.transform.node.Node import org.jitsi.nlj.transform.node.PacketCacher @@ -108,6 +109,7 @@ class RtpSenderImpl( private val srtpEncryptWrapper = SrtpEncryptNode() private val srtcpEncryptWrapper = SrtcpEncryptNode() private val toggleablePcapWriter = ToggleablePcapWriter(logger, "$id-tx") + private val compliancePcapWriter = CompliancePcapWriter(logger, streamInformationStore, id, "fe") private val outgoingPacketCache = PacketCacher() private val absSendTime = AbsSendTime(streamInformationStore) private val statsTracker = OutgoingStatisticsTracker() @@ -148,6 +150,7 @@ class RtpSenderImpl( node(TccSeqNumTagger(transportCcEngine, streamInformationStore)) node(HeaderExtEncoder(streamInformationStore, logger)) node(toggleablePcapWriter.newObserverNode()) + node(compliancePcapWriter.newObserverNode()) node(srtpEncryptWrapper) node(packetStreamStats.createNewNode()) node(PacketLossNode(packetLossConfig), condition = { packetLossConfig.enabled }) @@ -321,6 +324,11 @@ class RtpSenderImpl( NodeTeardownVisitor().reverseVisit(outputPipelineTerminationNode) incomingPacketQueue.close() toggleablePcapWriter.disable() + compliancePcapWriter.disable() + } + + override fun setComplianceRecording(comRec: String?, contextId: String?) { + compliancePcapWriter.configure(comRec, contextId) } companion object { diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/Transceiver.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/Transceiver.kt index d2302cf63f..bba80adfeb 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/Transceiver.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/Transceiver.kt @@ -379,6 +379,11 @@ class Transceiver( return rtpReceiver.isFeatureEnabled(feature) } + fun setComplianceRecording(comRec: String?, contextId: String?) { + rtpReceiver.setComplianceRecording(comRec, contextId) + rtpSender.setComplianceRecording(comRec, contextId) + } + companion object { init { // Node.plugins.add(BufferTracePlugin) diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt new file mode 100644 index 0000000000..ae6a5a7529 --- /dev/null +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt @@ -0,0 +1,264 @@ +/* + * Copyright @ 2018 - Present, 8x8 Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jitsi.nlj.transform.node + +import com.google.gson.GsonBuilder +import com.google.gson.JsonElement +import com.google.gson.JsonParser +import org.jitsi.config.JitsiConfig +import org.jitsi.metaconfig.config +import org.jitsi.metaconfig.from +import org.jitsi.nlj.PacketInfo +import org.jitsi.nlj.rtp.AudioRtpPacket +import org.jitsi.nlj.rtp.VideoRtpPacket +import org.jitsi.nlj.util.ReadOnlyStreamInformationStore +import org.jitsi.utils.MediaType +import org.jitsi.utils.logging2.Logger +import org.json.simple.JSONArray +import org.json.simple.JSONObject +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths + +class CompliancePcapWriter( + private val logger: Logger, + private val streamInformationStore: ReadOnlyStreamInformationStore, + private val id: String, + private val captureEnd: String +) { + private var writer: PcapWriter? = null + private val lock = Any() + private var contextId: String? = null + private var mode: String = CAP_MODE_NONE + + private var captureAudio = false + private var captureVideo = false + + fun capId(): String { + return "${id}_$captureEnd" + } + + fun filename(): String { + return "$basePath/${contextId}_${capId()}" + } + + fun pcapFilename(): String { + return "${filename()}.pcap" + } + + fun jsonFilename(): String { + return "${filename()}.json" + } + + fun validJmtConfig(): Boolean { + if (!allowed) { + logger.info("Compliance recording:${capId()} is not allowed in jmt.compliance-recording.enabled") + return false + } + + if (basePath.isEmpty()) { + logger.error("Compliance recording:${capId()} jmt.compliance-recording.base-path is not configured") + return false + } + + if (!Files.isDirectory(Paths.get(basePath))) { + logger.error("Compliance recording:${capId()} base-path:$basePath is not a valid directory path") + return false + } + + return true + } + + fun writeMetadata(): Boolean { + val meta = JSONObject() + + meta.put("endpoint_id", id) + meta.put("context_id", contextId) + meta.put("capture_end", captureEnd) + meta.put("capture_mode", mode) + + val payloadsMap = JSONArray() + streamInformationStore.rtpPayloadTypes.forEach { + if ((it.value.mediaType == MediaType.AUDIO && (mode == CAP_MODE_AUDIO || mode == CAP_MODE_AUDIO_VIDEO)) || + (it.value.mediaType == MediaType.VIDEO && (mode == CAP_MODE_VIDEO || mode == CAP_MODE_AUDIO_VIDEO)) + ) { + val entry = JSONObject() + entry["payload_type"] = it.value.pt + entry["encoding"] = it.value.encoding.toString().lowercase() + entry["media_type"] = it.value.mediaType.name.lowercase() + entry["clock_rate"] = it.value.clockRate + payloadsMap.add(entry) + } + } + + meta.put("payload_map", payloadsMap) + + // json-simple produces one long flat line + // File(jsonFilename()).writeText(meta.toJSONString()) + + // gson produces elements on separate lines + val gson = GsonBuilder().setPrettyPrinting().create() + val je: JsonElement = JsonParser.parseString(meta.toJSONString()) + + try { + File(jsonFilename()).writeText(gson.toJson(je)) + } catch (ex: Exception) { + logger.error("Compliance recording:${capId()} exception: $ex") + return false + } + + return true + } + + fun configure(newMode: String?, newContextId: String?) { + + // TODO - how to trace long line exceeding 120 chars + logger.info("Compliance recording:${capId()} configure mode:$mode -> $newMode") + logger.info("Compliance recording:${capId()} contextId:$contextId -> $newContextId") + + // Disable conditions + if (newMode.isNullOrEmpty() || newMode == CAP_MODE_NONE) { + disable() + synchronized(lock) { + mode = "none" + contextId = null + } + return + } + + // Checks for valid enable conditions + if (!validJmtConfig()) { + return + } + + if (newMode !in listOf(CAP_MODE_AUDIO, CAP_MODE_VIDEO, CAP_MODE_AUDIO_VIDEO)) { + logger.error("Compliance recording:${capId()} invalid mode:$newMode") + return + } + + if (newContextId.isNullOrEmpty()) { + logger.error("Compliance recording:${capId()} invalid context-id:$newContextId") + return + } + + synchronized(lock) { + if (mode == newMode && contextId == newContextId) { + // nothing to do + logger.warn("Compliance recording:${capId()} duplicate configuration request ignored.") + return + } + } + + if (isEnabled()) { + // reconfiguring to different mode on the fly? + logger.warn("Compliance recording:${capId()} is already enabled - resetting by disabling first.") + disable() + } + + synchronized(lock) { + mode = newMode + contextId = newContextId + if (!writeMetadata()) { + contextId = null + mode = "none" + return + } + } + + // Enable / open the writer with the new mode and context-id + enable() + } + + fun enable() { + logger.info("Compliance recording:${capId()} enable ${pcapFilename()}") + + if (!validJmtConfig()) { + return + } + + if (contextId.isNullOrEmpty()) { + logger.error("Compliance recording:${capId()} is not configured with context-id") + return + } + + synchronized(lock) { + captureAudio = false + captureVideo = false + when (mode) { + CAP_MODE_AUDIO -> captureAudio = true + CAP_MODE_VIDEO -> captureVideo = true + CAP_MODE_AUDIO_VIDEO -> { + captureAudio = true + captureVideo = true + } + else -> { + logger.error("Compliance recording:${capId()} invalid mode:$mode") + return + } + } + + if (writer == null) { + logger.info("Compliance recording:${capId()} enable ${pcapFilename()}") + writer = PcapWriter(logger, pcapFilename()) + } + } + } + + fun disable() { + synchronized(lock) { + captureAudio = false + captureVideo = false + + if (writer != null) { + logger.info("Compliance recording:${capId()} disable ${pcapFilename()}") + writer?.close() + writer = null + } + } + } + + fun isEnabled(): Boolean = writer != null + + fun newObserverNode(): Node = PcapWriterNode("Compliance recording:${capId()} pcap writer") + + private inner class PcapWriterNode(name: String) : ObserverNode(name) { + override fun observe(packetInfo: PacketInfo) { + synchronized(lock) { + if (packetInfo.packet is AudioRtpPacket) { + if (captureAudio) { + writer?.processPacket(packetInfo) + } + } else if (packetInfo.packet is VideoRtpPacket) { + if (captureVideo) { + writer?.processPacket(packetInfo) + } + } + } + } + + override fun trace(f: () -> Unit) = f.invoke() + } + + companion object { + private val allowed: Boolean by config("jmt.compliance-recording.enabled".from(JitsiConfig.newConfig)) + private val basePath: String by config("jmt.compliance-recording.base-path".from(JitsiConfig.newConfig)) + + private const val CAP_MODE_NONE = "none" + private const val CAP_MODE_AUDIO = "audio" + private const val CAP_MODE_VIDEO = "video" + private const val CAP_MODE_AUDIO_VIDEO = "audio-video" + } +} diff --git a/jitsi-media-transform/src/main/resources/reference.conf b/jitsi-media-transform/src/main/resources/reference.conf index 9f7a5e252a..b5790b7e25 100644 --- a/jitsi-media-transform/src/main/resources/reference.conf +++ b/jitsi-media-transform/src/main/resources/reference.conf @@ -160,4 +160,12 @@ jmt { } } } + + compliance-recording { + enabled=true + + // Base path for storing the compliance recording pcap files + base-path="/tmp/com-rec" + } + } diff --git a/jvb/src/main/kotlin/org/jitsi/videobridge/colibri2/Colibri2ConferenceHandler.kt b/jvb/src/main/kotlin/org/jitsi/videobridge/colibri2/Colibri2ConferenceHandler.kt index 8e65fb4f1b..7fa0c99cb3 100644 --- a/jvb/src/main/kotlin/org/jitsi/videobridge/colibri2/Colibri2ConferenceHandler.kt +++ b/jvb/src/main/kotlin/org/jitsi/videobridge/colibri2/Colibri2ConferenceHandler.kt @@ -240,6 +240,17 @@ class Colibri2ConferenceHandler( /* No need to put media in conference-modified. */ } + // Configure compliance recording after the payload types maps are set + if (c2endpoint.create) { + val comRec = c2endpoint.getAttributeAsString("com-rec") + logger.info("com-rec value: $comRec") + + val contextId = c2endpoint.getAttributeAsString("context-id") + logger.info("context-id value: $contextId") + + endpoint.transceiver.setComplianceRecording(comRec, contextId) + } + endpoint.acceptAudio = endpoint.transceiver.readOnlyStreamInformationStore.rtpPayloadTypes.values.any { it.mediaType == MediaType.AUDIO } From 41f3359a228f999eab0ba112462328c790ecc507 Mon Sep 17 00:00:00 2001 From: Danail Kirov Date: Mon, 13 Nov 2023 14:52:02 -0800 Subject: [PATCH 2/7] fix ktlint check --- .../kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt index ae6a5a7529..7acd65c311 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt @@ -124,7 +124,6 @@ class CompliancePcapWriter( } fun configure(newMode: String?, newContextId: String?) { - // TODO - how to trace long line exceeding 120 chars logger.info("Compliance recording:${capId()} configure mode:$mode -> $newMode") logger.info("Compliance recording:${capId()} contextId:$contextId -> $newContextId") From e73e58186073f51ea0c4951dfac6fbbc3436bacf Mon Sep 17 00:00:00 2001 From: Danail Kirov Date: Mon, 13 Nov 2023 15:53:55 -0800 Subject: [PATCH 3/7] Removed "Compliance recording:" prefix --- .../transform/node/CompliancePcapWriter.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt index 7acd65c311..6ef4fd06ae 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt @@ -65,17 +65,17 @@ class CompliancePcapWriter( fun validJmtConfig(): Boolean { if (!allowed) { - logger.info("Compliance recording:${capId()} is not allowed in jmt.compliance-recording.enabled") + logger.info("${capId()} is not allowed in jmt.compliance-recording.enabled") return false } if (basePath.isEmpty()) { - logger.error("Compliance recording:${capId()} jmt.compliance-recording.base-path is not configured") + logger.error("${capId()} jmt.compliance-recording.base-path is not configured") return false } if (!Files.isDirectory(Paths.get(basePath))) { - logger.error("Compliance recording:${capId()} base-path:$basePath is not a valid directory path") + logger.error("${capId()} base-path:$basePath is not a valid directory path") return false } @@ -116,7 +116,7 @@ class CompliancePcapWriter( try { File(jsonFilename()).writeText(gson.toJson(je)) } catch (ex: Exception) { - logger.error("Compliance recording:${capId()} exception: $ex") + logger.error("${capId()} exception: $ex") return false } @@ -125,8 +125,8 @@ class CompliancePcapWriter( fun configure(newMode: String?, newContextId: String?) { // TODO - how to trace long line exceeding 120 chars - logger.info("Compliance recording:${capId()} configure mode:$mode -> $newMode") - logger.info("Compliance recording:${capId()} contextId:$contextId -> $newContextId") + logger.info("${capId()} configure mode:$mode -> $newMode") + logger.info("${capId()} contextId:$contextId -> $newContextId") // Disable conditions if (newMode.isNullOrEmpty() || newMode == CAP_MODE_NONE) { @@ -144,26 +144,26 @@ class CompliancePcapWriter( } if (newMode !in listOf(CAP_MODE_AUDIO, CAP_MODE_VIDEO, CAP_MODE_AUDIO_VIDEO)) { - logger.error("Compliance recording:${capId()} invalid mode:$newMode") + logger.error("${capId()} invalid mode:$newMode") return } if (newContextId.isNullOrEmpty()) { - logger.error("Compliance recording:${capId()} invalid context-id:$newContextId") + logger.error("${capId()} invalid context-id:$newContextId") return } synchronized(lock) { if (mode == newMode && contextId == newContextId) { // nothing to do - logger.warn("Compliance recording:${capId()} duplicate configuration request ignored.") + logger.warn("${capId()} duplicate configuration request ignored.") return } } if (isEnabled()) { // reconfiguring to different mode on the fly? - logger.warn("Compliance recording:${capId()} is already enabled - resetting by disabling first.") + logger.warn("${capId()} is already enabled - resetting by disabling first.") disable() } @@ -182,14 +182,14 @@ class CompliancePcapWriter( } fun enable() { - logger.info("Compliance recording:${capId()} enable ${pcapFilename()}") + logger.info("${capId()} enable ${pcapFilename()}") if (!validJmtConfig()) { return } if (contextId.isNullOrEmpty()) { - logger.error("Compliance recording:${capId()} is not configured with context-id") + logger.error("${capId()} is not configured with context-id") return } @@ -204,13 +204,13 @@ class CompliancePcapWriter( captureVideo = true } else -> { - logger.error("Compliance recording:${capId()} invalid mode:$mode") + logger.error("${capId()} invalid mode:$mode") return } } if (writer == null) { - logger.info("Compliance recording:${capId()} enable ${pcapFilename()}") + logger.info("${capId()} enable ${pcapFilename()}") writer = PcapWriter(logger, pcapFilename()) } } @@ -222,7 +222,7 @@ class CompliancePcapWriter( captureVideo = false if (writer != null) { - logger.info("Compliance recording:${capId()} disable ${pcapFilename()}") + logger.info("${capId()} disable ${pcapFilename()}") writer?.close() writer = null } @@ -231,7 +231,7 @@ class CompliancePcapWriter( fun isEnabled(): Boolean = writer != null - fun newObserverNode(): Node = PcapWriterNode("Compliance recording:${capId()} pcap writer") + fun newObserverNode(): Node = PcapWriterNode("${capId()} compliance pcap writer") private inner class PcapWriterNode(name: String) : ObserverNode(name) { override fun observe(packetInfo: PacketInfo) { From 09e715ea937c52c358e295e0c8b0f11e201f0856 Mon Sep 17 00:00:00 2001 From: Danail Kirov Date: Wed, 15 Nov 2023 17:42:06 -0800 Subject: [PATCH 4/7] use jackson for pretty printing --- jitsi-media-transform/pom.xml | 5 ++++ .../transform/node/CompliancePcapWriter.kt | 26 ++++++++++++------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/jitsi-media-transform/pom.xml b/jitsi-media-transform/pom.xml index 2a46636c36..3c7ffcaed5 100644 --- a/jitsi-media-transform/pom.xml +++ b/jitsi-media-transform/pom.xml @@ -48,6 +48,11 @@ ${project.groupId} jitsi-metaconfig + + com.fasterxml.jackson.module + jackson-module-kotlin + ${jackson.version} + org.jetbrains.kotlin kotlin-reflect diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt index 6ef4fd06ae..75844d11a1 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt @@ -15,9 +15,7 @@ */ package org.jitsi.nlj.transform.node -import com.google.gson.GsonBuilder -import com.google.gson.JsonElement -import com.google.gson.JsonParser +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.jitsi.config.JitsiConfig import org.jitsi.metaconfig.config import org.jitsi.metaconfig.from @@ -107,14 +105,22 @@ class CompliancePcapWriter( meta.put("payload_map", payloadsMap) // json-simple produces one long flat line - // File(jsonFilename()).writeText(meta.toJSONString()) - - // gson produces elements on separate lines - val gson = GsonBuilder().setPrettyPrinting().create() - val je: JsonElement = JsonParser.parseString(meta.toJSONString()) - +// File(jsonFilename()).writeText(meta.toJSONString()) + + // gson pretty printing +// try { +// val gson = GsonBuilder().setPrettyPrinting().create() +// val je: JsonElement = JsonParser.parseString(meta.toJSONString()) +// File(jsonFilename()).writeText(gson.toJson(je)) +// } catch (ex: Exception) { +// logger.error("${capId()} exception: $ex") +// return false +// } + + // jackson pretty printing try { - File(jsonFilename()).writeText(gson.toJson(je)) + val writer = jacksonObjectMapper().writerWithDefaultPrettyPrinter() + File(jsonFilename()).writeText(writer.writeValueAsString(meta)) } catch (ex: Exception) { logger.error("${capId()} exception: $ex") return false From c5e65be466e3fd71e0ea30635c3f93ba59cf1ad3 Mon Sep 17 00:00:00 2001 From: Danail Kirov Date: Thu, 16 Nov 2023 16:40:41 -0800 Subject: [PATCH 5/7] Rename file --- .../node/{CompliancePcapWriter.kt => MetadataPcapWriter.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/{CompliancePcapWriter.kt => MetadataPcapWriter.kt} (100%) diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/MetadataPcapWriter.kt similarity index 100% rename from jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/CompliancePcapWriter.kt rename to jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/MetadataPcapWriter.kt From 492d9792ad87a37b90494e46de308f47effb68a2 Mon Sep 17 00:00:00 2001 From: Danail Kirov Date: Thu, 16 Nov 2023 16:53:17 -0800 Subject: [PATCH 6/7] added associate_pt and renamed file and methods --- .../main/kotlin/org/jitsi/nlj/RtpReceiver.kt | 2 +- .../kotlin/org/jitsi/nlj/RtpReceiverImpl.kt | 12 +++--- .../main/kotlin/org/jitsi/nlj/RtpSender.kt | 2 +- .../kotlin/org/jitsi/nlj/RtpSenderImpl.kt | 12 +++--- .../main/kotlin/org/jitsi/nlj/Transceiver.kt | 6 +-- .../nlj/transform/node/MetadataPcapWriter.kt | 38 +++++++++---------- .../src/main/resources/reference.conf | 7 ++-- .../colibri2/Colibri2ConferenceHandler.kt | 8 ++-- 8 files changed, 41 insertions(+), 46 deletions(-) diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiver.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiver.kt index 067525b51b..57f9d696cb 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiver.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiver.kt @@ -64,7 +64,7 @@ abstract class RtpReceiver : abstract fun forceMuteVideo(shouldMute: Boolean) - abstract fun setComplianceRecording(comRec: String?, contextId: String?) + abstract fun setPcapRecording(mode: String?, contextId: String?) } interface RtpReceiverEventHandler { diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiverImpl.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiverImpl.kt index 0f1d4178b3..73700757b7 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiverImpl.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiverImpl.kt @@ -32,8 +32,8 @@ import org.jitsi.nlj.stats.RtpReceiverStats import org.jitsi.nlj.transform.NodeEventVisitor import org.jitsi.nlj.transform.NodeStatsVisitor import org.jitsi.nlj.transform.NodeTeardownVisitor -import org.jitsi.nlj.transform.node.CompliancePcapWriter import org.jitsi.nlj.transform.node.ConsumerNode +import org.jitsi.nlj.transform.node.MetadataPcapWriter import org.jitsi.nlj.transform.node.Node import org.jitsi.nlj.transform.node.PacketLossConfig import org.jitsi.nlj.transform.node.PacketLossNode @@ -141,7 +141,7 @@ class RtpReceiverImpl @JvmOverloads constructor( }) } private val toggleablePcapWriter = ToggleablePcapWriter(logger, "$id-rx") - private val compliancePcapWriter = CompliancePcapWriter(logger, streamInformationStore, id, "ne") + private val metadataPcapWriter = MetadataPcapWriter(logger, streamInformationStore, id, "ne") private val videoBitrateCalculator = VideoBitrateCalculator(parentLogger) private val audioBitrateCalculator = BitrateCalculator("Audio bitrate calculator") @@ -227,7 +227,7 @@ class RtpReceiverImpl @JvmOverloads constructor( // This reads audio levels from packets that use cryptex. TODO: should it go in the Audio path? node(audioLevelReader.postDecryptNode) node(toggleablePcapWriter.newObserverNode()) - node(compliancePcapWriter.newObserverNode()) + node(metadataPcapWriter.newObserverNode()) node(statsTracker) node(PaddingTermination(logger)) demux("Media Type") { @@ -353,14 +353,14 @@ class RtpReceiverImpl @JvmOverloads constructor( NodeTeardownVisitor().visit(inputTreeRoot) incomingPacketQueue.close() toggleablePcapWriter.disable() - compliancePcapWriter.disable() + metadataPcapWriter.disable() } override fun onRttUpdate(newRttMs: Double) { remoteBandwidthEstimator.onRttUpdate(newRttMs) } - override fun setComplianceRecording(comRec: String?, contextId: String?) { - compliancePcapWriter.configure(comRec, contextId) + override fun setPcapRecording(mode: String?, contextId: String?) { + metadataPcapWriter.configure(mode, contextId) } } diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSender.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSender.kt index 77de1ba99f..264d43820e 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSender.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSender.kt @@ -47,7 +47,7 @@ abstract class RtpSender : abstract fun setFeature(feature: Features, enabled: Boolean) abstract fun isFeatureEnabled(feature: Features): Boolean abstract fun tearDown() - abstract fun setComplianceRecording(comRec: String?, contextId: String?) + abstract fun setPcapRecording(mode: String?, contextId: String?) abstract val bandwidthEstimator: BandwidthEstimator } diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSenderImpl.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSenderImpl.kt index 19e067513d..26e86aa82a 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSenderImpl.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSenderImpl.kt @@ -32,8 +32,8 @@ import org.jitsi.nlj.transform.NodeEventVisitor import org.jitsi.nlj.transform.NodeStatsVisitor import org.jitsi.nlj.transform.NodeTeardownVisitor import org.jitsi.nlj.transform.node.AudioRedHandler -import org.jitsi.nlj.transform.node.CompliancePcapWriter import org.jitsi.nlj.transform.node.ConsumerNode +import org.jitsi.nlj.transform.node.MetadataPcapWriter import org.jitsi.nlj.transform.node.Node import org.jitsi.nlj.transform.node.PacketCacher import org.jitsi.nlj.transform.node.PacketLossConfig @@ -109,7 +109,7 @@ class RtpSenderImpl( private val srtpEncryptWrapper = SrtpEncryptNode() private val srtcpEncryptWrapper = SrtcpEncryptNode() private val toggleablePcapWriter = ToggleablePcapWriter(logger, "$id-tx") - private val compliancePcapWriter = CompliancePcapWriter(logger, streamInformationStore, id, "fe") + private val metadataPcapWriter = MetadataPcapWriter(logger, streamInformationStore, id, "fe") private val outgoingPacketCache = PacketCacher() private val absSendTime = AbsSendTime(streamInformationStore) private val statsTracker = OutgoingStatisticsTracker() @@ -150,7 +150,7 @@ class RtpSenderImpl( node(TccSeqNumTagger(transportCcEngine, streamInformationStore)) node(HeaderExtEncoder(streamInformationStore, logger)) node(toggleablePcapWriter.newObserverNode()) - node(compliancePcapWriter.newObserverNode()) + node(metadataPcapWriter.newObserverNode()) node(srtpEncryptWrapper) node(packetStreamStats.createNewNode()) node(PacketLossNode(packetLossConfig), condition = { packetLossConfig.enabled }) @@ -324,11 +324,11 @@ class RtpSenderImpl( NodeTeardownVisitor().reverseVisit(outputPipelineTerminationNode) incomingPacketQueue.close() toggleablePcapWriter.disable() - compliancePcapWriter.disable() + metadataPcapWriter.disable() } - override fun setComplianceRecording(comRec: String?, contextId: String?) { - compliancePcapWriter.configure(comRec, contextId) + override fun setPcapRecording(mode: String?, contextId: String?) { + metadataPcapWriter.configure(mode, contextId) } companion object { diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/Transceiver.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/Transceiver.kt index bba80adfeb..1958696d07 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/Transceiver.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/Transceiver.kt @@ -379,9 +379,9 @@ class Transceiver( return rtpReceiver.isFeatureEnabled(feature) } - fun setComplianceRecording(comRec: String?, contextId: String?) { - rtpReceiver.setComplianceRecording(comRec, contextId) - rtpSender.setComplianceRecording(comRec, contextId) + fun setPcapRecording(mode: String?, contextId: String?) { + rtpReceiver.setPcapRecording(mode, contextId) + rtpSender.setPcapRecording(mode, contextId) } companion object { diff --git a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/MetadataPcapWriter.kt b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/MetadataPcapWriter.kt index 75844d11a1..afec32f216 100644 --- a/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/MetadataPcapWriter.kt +++ b/jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/MetadataPcapWriter.kt @@ -31,7 +31,7 @@ import java.io.File import java.nio.file.Files import java.nio.file.Paths -class CompliancePcapWriter( +class MetadataPcapWriter( private val logger: Logger, private val streamInformationStore: ReadOnlyStreamInformationStore, private val id: String, @@ -63,12 +63,12 @@ class CompliancePcapWriter( fun validJmtConfig(): Boolean { if (!allowed) { - logger.info("${capId()} is not allowed in jmt.compliance-recording.enabled") + logger.info("${capId()} is not allowed in jmt.metadata-pcap-recording.enabled") return false } if (basePath.isEmpty()) { - logger.error("${capId()} jmt.compliance-recording.base-path is not configured") + logger.error("${capId()} jmt.metadata-pcap-recording.base-path is not configured") return false } @@ -94,31 +94,27 @@ class CompliancePcapWriter( (it.value.mediaType == MediaType.VIDEO && (mode == CAP_MODE_VIDEO || mode == CAP_MODE_AUDIO_VIDEO)) ) { val entry = JSONObject() + val encoding = it.value.encoding.toString().lowercase() entry["payload_type"] = it.value.pt - entry["encoding"] = it.value.encoding.toString().lowercase() + entry["encoding"] = encoding entry["media_type"] = it.value.mediaType.name.lowercase() entry["clock_rate"] = it.value.clockRate + if (encoding == "rtx") { + val apt = it.value.parameters["apt"] + entry["associated_pt"] = apt?.toInt() ?: -1 + if (apt == null) { + logger.error("${capId()} no associated payload type for rtx:${it.value.pt}") + } + } + payloadsMap.add(entry) } } meta.put("payload_map", payloadsMap) - // json-simple produces one long flat line -// File(jsonFilename()).writeText(meta.toJSONString()) - - // gson pretty printing -// try { -// val gson = GsonBuilder().setPrettyPrinting().create() -// val je: JsonElement = JsonParser.parseString(meta.toJSONString()) -// File(jsonFilename()).writeText(gson.toJson(je)) -// } catch (ex: Exception) { -// logger.error("${capId()} exception: $ex") -// return false -// } - - // jackson pretty printing try { + // jackson pretty printing val writer = jacksonObjectMapper().writerWithDefaultPrettyPrinter() File(jsonFilename()).writeText(writer.writeValueAsString(meta)) } catch (ex: Exception) { @@ -237,7 +233,7 @@ class CompliancePcapWriter( fun isEnabled(): Boolean = writer != null - fun newObserverNode(): Node = PcapWriterNode("${capId()} compliance pcap writer") + fun newObserverNode(): Node = PcapWriterNode("${capId()} metadata pcap writer") private inner class PcapWriterNode(name: String) : ObserverNode(name) { override fun observe(packetInfo: PacketInfo) { @@ -258,8 +254,8 @@ class CompliancePcapWriter( } companion object { - private val allowed: Boolean by config("jmt.compliance-recording.enabled".from(JitsiConfig.newConfig)) - private val basePath: String by config("jmt.compliance-recording.base-path".from(JitsiConfig.newConfig)) + private val allowed: Boolean by config("jmt.metadata-pcap-recording.enabled".from(JitsiConfig.newConfig)) + private val basePath: String by config("jmt.metadata-pcap-recording.base-path".from(JitsiConfig.newConfig)) private const val CAP_MODE_NONE = "none" private const val CAP_MODE_AUDIO = "audio" diff --git a/jitsi-media-transform/src/main/resources/reference.conf b/jitsi-media-transform/src/main/resources/reference.conf index b5790b7e25..8f374298df 100644 --- a/jitsi-media-transform/src/main/resources/reference.conf +++ b/jitsi-media-transform/src/main/resources/reference.conf @@ -161,11 +161,10 @@ jmt { } } - compliance-recording { + metadata-pcap-recording { enabled=true - - // Base path for storing the compliance recording pcap files - base-path="/tmp/com-rec" + // Base path for storing the pcap and metadata files + base-path="/tmp/jvb-rec" } } diff --git a/jvb/src/main/kotlin/org/jitsi/videobridge/colibri2/Colibri2ConferenceHandler.kt b/jvb/src/main/kotlin/org/jitsi/videobridge/colibri2/Colibri2ConferenceHandler.kt index 7fa0c99cb3..7dda72fa81 100644 --- a/jvb/src/main/kotlin/org/jitsi/videobridge/colibri2/Colibri2ConferenceHandler.kt +++ b/jvb/src/main/kotlin/org/jitsi/videobridge/colibri2/Colibri2ConferenceHandler.kt @@ -240,15 +240,15 @@ class Colibri2ConferenceHandler( /* No need to put media in conference-modified. */ } - // Configure compliance recording after the payload types maps are set + // Configure pcap recording after the payload types maps are set if (c2endpoint.create) { - val comRec = c2endpoint.getAttributeAsString("com-rec") - logger.info("com-rec value: $comRec") + val mode = c2endpoint.getAttributeAsString("pcap-rec-mode") + logger.info("pcap-rec-mode value: $mode") val contextId = c2endpoint.getAttributeAsString("context-id") logger.info("context-id value: $contextId") - endpoint.transceiver.setComplianceRecording(comRec, contextId) + endpoint.transceiver.setPcapRecording(mode, contextId) } endpoint.acceptAudio = endpoint.transceiver.readOnlyStreamInformationStore.rtpPayloadTypes.values.any { From efd9993f4a1fa48edc07caf4ab1fd105bf56e497 Mon Sep 17 00:00:00 2001 From: Danail Kirov Date: Thu, 16 Nov 2023 18:03:18 -0800 Subject: [PATCH 7/7] removed gson dependency --- jitsi-media-transform/pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/jitsi-media-transform/pom.xml b/jitsi-media-transform/pom.xml index 3c7ffcaed5..faeca5da5a 100644 --- a/jitsi-media-transform/pom.xml +++ b/jitsi-media-transform/pom.xml @@ -140,12 +140,6 @@ 2.3.1 test - - com.google.code.gson - gson - 2.9.0 - compile -