From 26077d7972f39cff7dd75c3419fa4cfff6f4ccfe Mon Sep 17 00:00:00 2001 From: pedro Date: Wed, 6 Mar 2024 13:16:37 +0100 Subject: [PATCH 1/7] support udp protocol with mpeg2-ts --- README.md | 7 + library/build.gradle.kts | 1 + .../com/pedro/library/generic/ClientType.kt | 2 +- .../pedro/library/generic/GenericCamera1.kt | 47 +-- .../pedro/library/generic/GenericCamera2.kt | 47 +-- .../pedro/library/generic/GenericDisplay.kt | 46 +-- .../pedro/library/generic/GenericFromFile.kt | 47 +-- .../pedro/library/generic/GenericOnlyAudio.kt | 31 +- .../pedro/library/generic/GenericStream.kt | 20 +- .../util/streamclient/GenericStreamClient.kt | 15 + .../util/streamclient/UdpStreamClient.kt | 100 ++++++ settings.gradle.kts | 2 +- udp/.gitignore | 1 + udp/build.gradle.kts | 64 ++++ udp/consumer-rules.pro | 0 udp/proguard-rules.pro | 21 ++ udp/src/main/AndroidManifest.xml | 1 + .../main/java/com/pedro/udp/CommandManager.kt | 53 ++++ udp/src/main/java/com/pedro/udp/UdpClient.kt | 277 +++++++++++++++++ udp/src/main/java/com/pedro/udp/UdpSender.kt | 294 ++++++++++++++++++ .../java/com/pedro/udp/utils/UdpSocket.kt | 73 +++++ .../main/java/com/pedro/udp/utils/UdpType.kt | 34 ++ udp/src/test/java/android/util/Log.java | 53 ++++ .../java/com/pedro/udp/ExampleUnitTest.kt | 17 + 24 files changed, 1146 insertions(+), 107 deletions(-) create mode 100644 library/src/main/java/com/pedro/library/util/streamclient/UdpStreamClient.kt create mode 100644 udp/.gitignore create mode 100644 udp/build.gradle.kts create mode 100644 udp/consumer-rules.pro create mode 100644 udp/proguard-rules.pro create mode 100644 udp/src/main/AndroidManifest.xml create mode 100644 udp/src/main/java/com/pedro/udp/CommandManager.kt create mode 100644 udp/src/main/java/com/pedro/udp/UdpClient.kt create mode 100644 udp/src/main/java/com/pedro/udp/UdpSender.kt create mode 100644 udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt create mode 100644 udp/src/main/java/com/pedro/udp/utils/UdpType.kt create mode 100644 udp/src/test/java/android/util/Log.java create mode 100644 udp/src/test/java/com/pedro/udp/ExampleUnitTest.kt diff --git a/README.md b/README.md index 22a428ec0..fd0a590d2 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,13 @@ dependencies { - [X] Encrypt (AES128, AES192 and AES256) - [ ] SRT auth. +### UDP (beta): + +- [X] Get upload bandwidth used. +- [X] H264, H265, AAC and OPUS support. +- [X] Unicast, Multicast and Broadcast support. +- [X] MPEG-TS support. + https://haivision.github.io/srt-rfc/draft-sharabayko-srt.html diff --git a/library/build.gradle.kts b/library/build.gradle.kts index e4be45be0..a1a3970fe 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -58,5 +58,6 @@ dependencies { api(project(":rtmp")) api(project(":rtsp")) api(project(":srt")) + api(project(":udp")) api(project(":common")) } diff --git a/library/src/main/java/com/pedro/library/generic/ClientType.kt b/library/src/main/java/com/pedro/library/generic/ClientType.kt index fc46eb1fc..a94ae9d46 100644 --- a/library/src/main/java/com/pedro/library/generic/ClientType.kt +++ b/library/src/main/java/com/pedro/library/generic/ClientType.kt @@ -17,5 +17,5 @@ package com.pedro.library.generic internal enum class ClientType { - NONE, RTMP, RTSP, SRT + NONE, RTMP, RTSP, SRT, UDP } \ No newline at end of file diff --git a/library/src/main/java/com/pedro/library/generic/GenericCamera1.kt b/library/src/main/java/com/pedro/library/generic/GenericCamera1.kt index 775573c17..f8c337792 100644 --- a/library/src/main/java/com/pedro/library/generic/GenericCamera1.kt +++ b/library/src/main/java/com/pedro/library/generic/GenericCamera1.kt @@ -31,10 +31,12 @@ import com.pedro.library.util.streamclient.RtmpStreamClient import com.pedro.library.util.streamclient.RtspStreamClient import com.pedro.library.util.streamclient.SrtStreamClient import com.pedro.library.util.streamclient.StreamClientListener +import com.pedro.library.util.streamclient.UdpStreamClient import com.pedro.library.view.OpenGlView import com.pedro.rtmp.rtmp.RtmpClient import com.pedro.rtsp.rtsp.RtspClient import com.pedro.srt.srt.SrtClient +import com.pedro.udp.UdpClient import java.nio.ByteBuffer import java.util.Locale @@ -52,6 +54,7 @@ class GenericCamera1: Camera1Base { private lateinit var rtmpClient: RtmpClient private lateinit var rtspClient: RtspClient private lateinit var srtClient: SrtClient + private lateinit var udpClient: UdpClient private lateinit var streamClient: GenericStreamClient private lateinit var connectChecker: ConnectChecker private var connectedType = ClientType.NONE @@ -79,10 +82,12 @@ class GenericCamera1: Camera1Base { rtmpClient = RtmpClient(connectChecker) rtspClient = RtspClient(connectChecker) srtClient = SrtClient(connectChecker) + udpClient = UdpClient(connectChecker) streamClient = GenericStreamClient( RtmpStreamClient(rtmpClient, streamClientListener), RtspStreamClient(rtspClient, streamClientListener), - SrtStreamClient(srtClient, streamClientListener) + SrtStreamClient(srtClient, streamClientListener), + UdpStreamClient(udpClient, streamClientListener), ) } @@ -97,6 +102,7 @@ class GenericCamera1: Camera1Base { rtmpClient.setVideoCodec(codec) rtspClient.setVideoCodec(codec) srtClient.setVideoCodec(codec) + udpClient.setVideoCodec(codec) } override fun setAudioCodecImp(codec: AudioCodec) { @@ -106,25 +112,36 @@ class GenericCamera1: Camera1Base { rtmpClient.setAudioCodec(codec) rtspClient.setAudioCodec(codec) srtClient.setAudioCodec(codec) + udpClient.setAudioCodec(codec) } override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) { rtmpClient.setAudioInfo(sampleRate, isStereo) rtspClient.setAudioInfo(sampleRate, isStereo) srtClient.setAudioInfo(sampleRate, isStereo) + udpClient.setAudioInfo(sampleRate, isStereo) } override fun startStreamRtp(url: String) { streamClient.connecting(url) if (url.lowercase(Locale.getDefault()).startsWith("rtmp")) { connectedType = ClientType.RTMP - startStreamRtpRtmp(url) + if (videoEncoder.rotation == 90 || videoEncoder.rotation == 270) { + rtmpClient.setVideoResolution(videoEncoder.height, videoEncoder.width) + } else { + rtmpClient.setVideoResolution(videoEncoder.width, videoEncoder.height) + } + rtmpClient.setFps(videoEncoder.fps) + rtmpClient.connect(url) } else if (url.lowercase(Locale.getDefault()).startsWith("rtsp")) { connectedType = ClientType.RTSP - startStreamRtpRtsp(url) + rtspClient.connect(url) } else if (url.lowercase(Locale.getDefault()).startsWith("srt")) { connectedType = ClientType.SRT - startStreamRtpSrt(url) + srtClient.connect(url) + } else if (url.lowercase(Locale.getDefault()).startsWith("udp")) { + connectedType = ClientType.UDP + udpClient.connect(url) } else { onMainThreadHandler { connectChecker.onConnectionFailed("Unsupported protocol. Only support rtmp, rtsp and srt") @@ -132,29 +149,12 @@ class GenericCamera1: Camera1Base { } } - private fun startStreamRtpRtmp(url: String) { - if (videoEncoder.rotation == 90 || videoEncoder.rotation == 270) { - rtmpClient.setVideoResolution(videoEncoder.height, videoEncoder.width) - } else { - rtmpClient.setVideoResolution(videoEncoder.width, videoEncoder.height) - } - rtmpClient.setFps(videoEncoder.fps) - rtmpClient.connect(url) - } - - private fun startStreamRtpRtsp(url: String) { - rtspClient.connect(url) - } - - private fun startStreamRtpSrt(url: String) { - srtClient.connect(url) - } - override fun stopStreamRtp() { when (connectedType) { ClientType.RTMP -> rtmpClient.disconnect() ClientType.RTSP -> rtspClient.disconnect() ClientType.SRT -> srtClient.disconnect() + ClientType.UDP -> udpClient.disconnect() else -> {} } connectedType = ClientType.NONE @@ -165,6 +165,7 @@ class GenericCamera1: Camera1Base { ClientType.RTMP -> rtmpClient.sendAudio(aacBuffer, info) ClientType.RTSP -> rtspClient.sendAudio(aacBuffer, info) ClientType.SRT -> srtClient.sendAudio(aacBuffer, info) + ClientType.UDP -> udpClient.sendAudio(aacBuffer, info) else -> {} } } @@ -173,6 +174,7 @@ class GenericCamera1: Camera1Base { rtmpClient.setVideoInfo(sps, pps, vps) rtspClient.setVideoInfo(sps, pps, vps) srtClient.setVideoInfo(sps, pps, vps) + udpClient.setVideoInfo(sps, pps, vps) } override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { @@ -180,6 +182,7 @@ class GenericCamera1: Camera1Base { ClientType.RTMP -> rtmpClient.sendVideo(h264Buffer, info) ClientType.RTSP -> rtspClient.sendVideo(h264Buffer, info) ClientType.SRT -> srtClient.sendVideo(h264Buffer, info) + ClientType.UDP -> udpClient.sendVideo(h264Buffer, info) else -> {} } } diff --git a/library/src/main/java/com/pedro/library/generic/GenericCamera2.kt b/library/src/main/java/com/pedro/library/generic/GenericCamera2.kt index ab91d89d8..ca600c5d9 100644 --- a/library/src/main/java/com/pedro/library/generic/GenericCamera2.kt +++ b/library/src/main/java/com/pedro/library/generic/GenericCamera2.kt @@ -31,10 +31,12 @@ import com.pedro.library.util.streamclient.RtmpStreamClient import com.pedro.library.util.streamclient.RtspStreamClient import com.pedro.library.util.streamclient.SrtStreamClient import com.pedro.library.util.streamclient.StreamClientListener +import com.pedro.library.util.streamclient.UdpStreamClient import com.pedro.library.view.OpenGlView import com.pedro.rtmp.rtmp.RtmpClient import com.pedro.rtsp.rtsp.RtspClient import com.pedro.srt.srt.SrtClient +import com.pedro.udp.UdpClient import java.nio.ByteBuffer import java.util.Locale @@ -53,6 +55,7 @@ class GenericCamera2: Camera2Base { private lateinit var rtmpClient: RtmpClient private lateinit var rtspClient: RtspClient private lateinit var srtClient: SrtClient + private lateinit var udpClient: UdpClient private lateinit var streamClient: GenericStreamClient private lateinit var connectChecker: ConnectChecker private var connectedType = ClientType.NONE @@ -80,10 +83,12 @@ class GenericCamera2: Camera2Base { rtmpClient = RtmpClient(connectChecker) rtspClient = RtspClient(connectChecker) srtClient = SrtClient(connectChecker) + udpClient = UdpClient(connectChecker) streamClient = GenericStreamClient( RtmpStreamClient(rtmpClient, streamClientListener), RtspStreamClient(rtspClient, streamClientListener), - SrtStreamClient(srtClient, streamClientListener) + SrtStreamClient(srtClient, streamClientListener), + UdpStreamClient(udpClient, streamClientListener), ) } @@ -98,6 +103,7 @@ class GenericCamera2: Camera2Base { rtmpClient.setVideoCodec(codec) rtspClient.setVideoCodec(codec) srtClient.setVideoCodec(codec) + udpClient.setVideoCodec(codec) } override fun setAudioCodecImp(codec: AudioCodec) { @@ -107,25 +113,36 @@ class GenericCamera2: Camera2Base { rtmpClient.setAudioCodec(codec) rtspClient.setAudioCodec(codec) srtClient.setAudioCodec(codec) + udpClient.setAudioCodec(codec) } override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) { rtmpClient.setAudioInfo(sampleRate, isStereo) rtspClient.setAudioInfo(sampleRate, isStereo) srtClient.setAudioInfo(sampleRate, isStereo) + udpClient.setAudioInfo(sampleRate, isStereo) } override fun startStreamRtp(url: String) { streamClient.connecting(url) if (url.lowercase(Locale.getDefault()).startsWith("rtmp")) { connectedType = ClientType.RTMP - startStreamRtpRtmp(url) + if (videoEncoder.rotation == 90 || videoEncoder.rotation == 270) { + rtmpClient.setVideoResolution(videoEncoder.height, videoEncoder.width) + } else { + rtmpClient.setVideoResolution(videoEncoder.width, videoEncoder.height) + } + rtmpClient.setFps(videoEncoder.fps) + rtmpClient.connect(url) } else if (url.lowercase(Locale.getDefault()).startsWith("rtsp")) { connectedType = ClientType.RTSP - startStreamRtpRtsp(url) + rtspClient.connect(url) } else if (url.lowercase(Locale.getDefault()).startsWith("srt")) { connectedType = ClientType.SRT - startStreamRtpSrt(url) + srtClient.connect(url) + } else if (url.lowercase(Locale.getDefault()).startsWith("udp")) { + connectedType = ClientType.UDP + udpClient.connect(url) } else { onMainThreadHandler { connectChecker.onConnectionFailed("Unsupported protocol. Only support rtmp, rtsp and srt") @@ -133,29 +150,12 @@ class GenericCamera2: Camera2Base { } } - private fun startStreamRtpRtmp(url: String) { - if (videoEncoder.rotation == 90 || videoEncoder.rotation == 270) { - rtmpClient.setVideoResolution(videoEncoder.height, videoEncoder.width) - } else { - rtmpClient.setVideoResolution(videoEncoder.width, videoEncoder.height) - } - rtmpClient.setFps(videoEncoder.fps) - rtmpClient.connect(url) - } - - private fun startStreamRtpRtsp(url: String) { - rtspClient.connect(url) - } - - private fun startStreamRtpSrt(url: String) { - srtClient.connect(url) - } - override fun stopStreamRtp() { when (connectedType) { ClientType.RTMP -> rtmpClient.disconnect() ClientType.RTSP -> rtspClient.disconnect() ClientType.SRT -> srtClient.disconnect() + ClientType.UDP -> udpClient.disconnect() else -> {} } connectedType = ClientType.NONE @@ -166,6 +166,7 @@ class GenericCamera2: Camera2Base { ClientType.RTMP -> rtmpClient.sendAudio(aacBuffer, info) ClientType.RTSP -> rtspClient.sendAudio(aacBuffer, info) ClientType.SRT -> srtClient.sendAudio(aacBuffer, info) + ClientType.UDP -> udpClient.sendAudio(aacBuffer, info) else -> {} } } @@ -174,6 +175,7 @@ class GenericCamera2: Camera2Base { rtmpClient.setVideoInfo(sps, pps, vps) rtspClient.setVideoInfo(sps, pps, vps) srtClient.setVideoInfo(sps, pps, vps) + udpClient.setVideoInfo(sps, pps, vps) } override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { @@ -181,6 +183,7 @@ class GenericCamera2: Camera2Base { ClientType.RTMP -> rtmpClient.sendVideo(h264Buffer, info) ClientType.RTSP -> rtspClient.sendVideo(h264Buffer, info) ClientType.SRT -> srtClient.sendVideo(h264Buffer, info) + ClientType.UDP -> udpClient.sendVideo(h264Buffer, info) else -> {} } } diff --git a/library/src/main/java/com/pedro/library/generic/GenericDisplay.kt b/library/src/main/java/com/pedro/library/generic/GenericDisplay.kt index a87324bae..d20a3a691 100644 --- a/library/src/main/java/com/pedro/library/generic/GenericDisplay.kt +++ b/library/src/main/java/com/pedro/library/generic/GenericDisplay.kt @@ -29,9 +29,11 @@ import com.pedro.library.util.streamclient.RtmpStreamClient import com.pedro.library.util.streamclient.RtspStreamClient import com.pedro.library.util.streamclient.SrtStreamClient import com.pedro.library.util.streamclient.StreamClientListener +import com.pedro.library.util.streamclient.UdpStreamClient import com.pedro.rtmp.rtmp.RtmpClient import com.pedro.rtsp.rtsp.RtspClient import com.pedro.srt.srt.SrtClient +import com.pedro.udp.UdpClient import java.nio.ByteBuffer @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @@ -49,10 +51,12 @@ class GenericDisplay( private val rtmpClient = RtmpClient(connectChecker) private val rtspClient = RtspClient(connectChecker) private val srtClient = SrtClient(connectChecker) + private val udpClient = UdpClient(connectChecker) private val streamClient = GenericStreamClient( RtmpStreamClient(rtmpClient, streamClientListener), RtspStreamClient(rtspClient, streamClientListener), - SrtStreamClient(srtClient, streamClientListener) + SrtStreamClient(srtClient, streamClientListener), + UdpStreamClient(udpClient, streamClientListener) ) private var connectedType = ClientType.NONE @@ -65,6 +69,7 @@ class GenericDisplay( rtmpClient.setVideoCodec(codec) rtspClient.setVideoCodec(codec) srtClient.setVideoCodec(codec) + udpClient.setVideoCodec(codec) } override fun setAudioCodecImp(codec: AudioCodec) { @@ -74,25 +79,36 @@ class GenericDisplay( rtmpClient.setAudioCodec(codec) rtspClient.setAudioCodec(codec) srtClient.setAudioCodec(codec) + udpClient.setAudioCodec(codec) } override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) { rtmpClient.setAudioInfo(sampleRate, isStereo) rtspClient.setAudioInfo(sampleRate, isStereo) srtClient.setAudioInfo(sampleRate, isStereo) + udpClient.setAudioInfo(sampleRate, isStereo) } override fun startStreamRtp(url: String) { streamClient.connecting(url) if (url.startsWith("rtmp", ignoreCase = true)) { connectedType = ClientType.RTMP - startStreamRtpRtmp(url) + if (videoEncoder.rotation == 90 || videoEncoder.rotation == 270) { + rtmpClient.setVideoResolution(videoEncoder.height, videoEncoder.width) + } else { + rtmpClient.setVideoResolution(videoEncoder.width, videoEncoder.height) + } + rtmpClient.setFps(videoEncoder.fps) + rtmpClient.connect(url) } else if (url.startsWith("rtsp", ignoreCase = true)) { connectedType = ClientType.RTSP - startStreamRtpRtsp(url) + rtspClient.connect(url) } else if (url.startsWith("srt", ignoreCase = true)) { connectedType = ClientType.SRT - startStreamRtpSrt(url) + srtClient.connect(url) + } else if (url.startsWith("udp", ignoreCase = true)) { + connectedType = ClientType.UDP + udpClient.connect(url) } else { onMainThreadHandler { connectChecker.onConnectionFailed("Unsupported protocol. Only support rtmp, rtsp and srt") @@ -100,29 +116,12 @@ class GenericDisplay( } } - private fun startStreamRtpRtmp(url: String) { - if (videoEncoder.rotation == 90 || videoEncoder.rotation == 270) { - rtmpClient.setVideoResolution(videoEncoder.height, videoEncoder.width) - } else { - rtmpClient.setVideoResolution(videoEncoder.width, videoEncoder.height) - } - rtmpClient.setFps(videoEncoder.fps) - rtmpClient.connect(url) - } - - private fun startStreamRtpRtsp(url: String) { - rtspClient.connect(url) - } - - private fun startStreamRtpSrt(url: String) { - srtClient.connect(url) - } - override fun stopStreamRtp() { when (connectedType) { ClientType.RTMP -> rtmpClient.disconnect() ClientType.RTSP -> rtspClient.disconnect() ClientType.SRT -> srtClient.disconnect() + ClientType.UDP -> udpClient.disconnect() else -> {} } connectedType = ClientType.NONE @@ -133,6 +132,7 @@ class GenericDisplay( ClientType.RTMP -> rtmpClient.sendAudio(aacBuffer, info) ClientType.RTSP -> rtspClient.sendAudio(aacBuffer, info) ClientType.SRT -> srtClient.sendAudio(aacBuffer, info) + ClientType.UDP -> udpClient.sendAudio(aacBuffer, info) else -> {} } } @@ -141,6 +141,7 @@ class GenericDisplay( rtmpClient.setVideoInfo(sps, pps, vps) rtspClient.setVideoInfo(sps, pps, vps) srtClient.setVideoInfo(sps, pps, vps) + udpClient.setVideoInfo(sps, pps, vps) } override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { @@ -148,6 +149,7 @@ class GenericDisplay( ClientType.RTMP -> rtmpClient.sendVideo(h264Buffer, info) ClientType.RTSP -> rtspClient.sendVideo(h264Buffer, info) ClientType.SRT -> srtClient.sendVideo(h264Buffer, info) + ClientType.UDP -> udpClient.sendVideo(h264Buffer, info) else -> {} } } diff --git a/library/src/main/java/com/pedro/library/generic/GenericFromFile.kt b/library/src/main/java/com/pedro/library/generic/GenericFromFile.kt index c50cff78e..7afac1fe9 100644 --- a/library/src/main/java/com/pedro/library/generic/GenericFromFile.kt +++ b/library/src/main/java/com/pedro/library/generic/GenericFromFile.kt @@ -31,10 +31,12 @@ import com.pedro.library.util.streamclient.RtmpStreamClient import com.pedro.library.util.streamclient.RtspStreamClient import com.pedro.library.util.streamclient.SrtStreamClient import com.pedro.library.util.streamclient.StreamClientListener +import com.pedro.library.util.streamclient.UdpStreamClient import com.pedro.library.view.OpenGlView import com.pedro.rtmp.rtmp.RtmpClient import com.pedro.rtsp.rtsp.RtspClient import com.pedro.srt.srt.SrtClient +import com.pedro.udp.UdpClient import java.nio.ByteBuffer @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) @@ -49,6 +51,7 @@ class GenericFromFile: FromFileBase { private lateinit var rtmpClient: RtmpClient private lateinit var rtspClient: RtspClient private lateinit var srtClient: SrtClient + private lateinit var udpClient: UdpClient private lateinit var streamClient: GenericStreamClient private var connectedType = ClientType.NONE @@ -78,10 +81,12 @@ class GenericFromFile: FromFileBase { rtmpClient = RtmpClient(connectChecker) rtspClient = RtspClient(connectChecker) srtClient = SrtClient(connectChecker) + udpClient = UdpClient(connectChecker) streamClient = GenericStreamClient( RtmpStreamClient(rtmpClient, streamClientListener), RtspStreamClient(rtspClient, streamClientListener), - SrtStreamClient(srtClient, streamClientListener) + SrtStreamClient(srtClient, streamClientListener), + UdpStreamClient(udpClient, streamClientListener), ) } @@ -94,6 +99,7 @@ class GenericFromFile: FromFileBase { rtmpClient.setVideoCodec(codec) rtspClient.setVideoCodec(codec) srtClient.setVideoCodec(codec) + udpClient.setVideoCodec(codec) } override fun setAudioCodecImp(codec: AudioCodec) { @@ -103,25 +109,36 @@ class GenericFromFile: FromFileBase { rtmpClient.setAudioCodec(codec) rtspClient.setAudioCodec(codec) srtClient.setAudioCodec(codec) + udpClient.setAudioCodec(codec) } override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) { rtmpClient.setAudioInfo(sampleRate, isStereo) rtspClient.setAudioInfo(sampleRate, isStereo) srtClient.setAudioInfo(sampleRate, isStereo) + udpClient.setAudioInfo(sampleRate, isStereo) } override fun startStreamRtp(url: String) { streamClient.connecting(url) if (url.startsWith("rtmp", ignoreCase = true)) { connectedType = ClientType.RTMP - startStreamRtpRtmp(url) + if (videoEncoder.rotation == 90 || videoEncoder.rotation == 270) { + rtmpClient.setVideoResolution(videoEncoder.height, videoEncoder.width) + } else { + rtmpClient.setVideoResolution(videoEncoder.width, videoEncoder.height) + } + rtmpClient.setFps(videoEncoder.fps) + rtmpClient.connect(url) } else if (url.startsWith("rtsp", ignoreCase = true)) { connectedType = ClientType.RTSP - startStreamRtpRtsp(url) + rtspClient.connect(url) } else if (url.startsWith("srt", ignoreCase = true)) { connectedType = ClientType.SRT - startStreamRtpSrt(url) + srtClient.connect(url) + } else if (url.startsWith("udp", ignoreCase = true)) { + connectedType = ClientType.UDP + udpClient.connect(url) } else { onMainThreadHandler { connectChecker.onConnectionFailed("Unsupported protocol. Only support rtmp, rtsp and srt") @@ -129,29 +146,12 @@ class GenericFromFile: FromFileBase { } } - private fun startStreamRtpRtmp(url: String) { - if (videoEncoder.rotation == 90 || videoEncoder.rotation == 270) { - rtmpClient.setVideoResolution(videoEncoder.height, videoEncoder.width) - } else { - rtmpClient.setVideoResolution(videoEncoder.width, videoEncoder.height) - } - rtmpClient.setFps(videoEncoder.fps) - rtmpClient.connect(url) - } - - private fun startStreamRtpRtsp(url: String) { - rtspClient.connect(url) - } - - private fun startStreamRtpSrt(url: String) { - srtClient.connect(url) - } - override fun stopStreamRtp() { when (connectedType) { ClientType.RTMP -> rtmpClient.disconnect() ClientType.RTSP -> rtspClient.disconnect() ClientType.SRT -> srtClient.disconnect() + ClientType.UDP -> udpClient.disconnect() else -> {} } connectedType = ClientType.NONE @@ -162,6 +162,7 @@ class GenericFromFile: FromFileBase { ClientType.RTMP -> rtmpClient.sendAudio(aacBuffer, info) ClientType.RTSP -> rtspClient.sendAudio(aacBuffer, info) ClientType.SRT -> srtClient.sendAudio(aacBuffer, info) + ClientType.UDP -> udpClient.sendAudio(aacBuffer, info) else -> {} } } @@ -170,6 +171,7 @@ class GenericFromFile: FromFileBase { rtmpClient.setVideoInfo(sps, pps, vps) rtspClient.setVideoInfo(sps, pps, vps) srtClient.setVideoInfo(sps, pps, vps) + udpClient.setVideoInfo(sps, pps, vps) } override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { @@ -177,6 +179,7 @@ class GenericFromFile: FromFileBase { ClientType.RTMP -> rtmpClient.sendVideo(h264Buffer, info) ClientType.RTSP -> rtspClient.sendVideo(h264Buffer, info) ClientType.SRT -> srtClient.sendVideo(h264Buffer, info) + ClientType.UDP -> udpClient.sendVideo(h264Buffer, info) else -> {} } } diff --git a/library/src/main/java/com/pedro/library/generic/GenericOnlyAudio.kt b/library/src/main/java/com/pedro/library/generic/GenericOnlyAudio.kt index 6647091a6..395cb20c3 100644 --- a/library/src/main/java/com/pedro/library/generic/GenericOnlyAudio.kt +++ b/library/src/main/java/com/pedro/library/generic/GenericOnlyAudio.kt @@ -24,9 +24,11 @@ import com.pedro.library.util.streamclient.GenericStreamClient import com.pedro.library.util.streamclient.RtmpStreamClient import com.pedro.library.util.streamclient.RtspStreamClient import com.pedro.library.util.streamclient.SrtStreamClient +import com.pedro.library.util.streamclient.UdpStreamClient import com.pedro.rtmp.rtmp.RtmpClient import com.pedro.rtsp.rtsp.RtspClient import com.pedro.srt.srt.SrtClient +import com.pedro.udp.UdpClient import java.nio.ByteBuffer class GenericOnlyAudio(private val connectChecker: ConnectChecker): OnlyAudioBase() { @@ -34,10 +36,12 @@ class GenericOnlyAudio(private val connectChecker: ConnectChecker): OnlyAudioBas private val rtmpClient = RtmpClient(connectChecker) private val rtspClient = RtspClient(connectChecker) private val srtClient = SrtClient(connectChecker) + private val udpClient = UdpClient(connectChecker) private val streamClient = GenericStreamClient( RtmpStreamClient(rtmpClient, null), RtspStreamClient(rtspClient, null), - SrtStreamClient(srtClient, null) + SrtStreamClient(srtClient, null), + UdpStreamClient(udpClient, null) ).apply { setOnlyAudio(true) } @@ -52,25 +56,30 @@ class GenericOnlyAudio(private val connectChecker: ConnectChecker): OnlyAudioBas rtmpClient.setAudioCodec(codec) rtspClient.setAudioCodec(codec) srtClient.setAudioCodec(codec) + udpClient.setAudioCodec(codec) } override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) { rtmpClient.setAudioInfo(sampleRate, isStereo) rtspClient.setAudioInfo(sampleRate, isStereo) srtClient.setAudioInfo(sampleRate, isStereo) + udpClient.setAudioInfo(sampleRate, isStereo) } override fun startStreamRtp(url: String) { streamClient.connecting(url) if (url.startsWith("rtmp", ignoreCase = true)) { connectedType = ClientType.RTMP - startStreamRtpRtmp(url) + rtmpClient.connect(url) } else if (url.startsWith("rtsp", ignoreCase = true)) { connectedType = ClientType.RTSP - startStreamRtpRtsp(url) + rtspClient.connect(url) } else if (url.startsWith("srt", ignoreCase = true)) { connectedType = ClientType.SRT - startStreamRtpSrt(url) + srtClient.connect(url) + } else if (url.startsWith("udp", ignoreCase = true)) { + connectedType = ClientType.UDP + udpClient.connect(url) } else { onMainThreadHandler { connectChecker.onConnectionFailed("Unsupported protocol. Only support rtmp, rtsp and srt") @@ -78,23 +87,12 @@ class GenericOnlyAudio(private val connectChecker: ConnectChecker): OnlyAudioBas } } - private fun startStreamRtpRtmp(url: String) { - rtmpClient.connect(url) - } - - private fun startStreamRtpRtsp(url: String) { - rtspClient.connect(url) - } - - private fun startStreamRtpSrt(url: String) { - srtClient.connect(url) - } - override fun stopStreamRtp() { when (connectedType) { ClientType.RTMP -> rtmpClient.disconnect() ClientType.RTSP -> rtspClient.disconnect() ClientType.SRT -> srtClient.disconnect() + ClientType.UDP -> udpClient.disconnect() else -> {} } connectedType = ClientType.NONE @@ -105,6 +103,7 @@ class GenericOnlyAudio(private val connectChecker: ConnectChecker): OnlyAudioBas ClientType.RTMP -> rtmpClient.sendAudio(aacBuffer, info) ClientType.RTSP -> rtspClient.sendAudio(aacBuffer, info) ClientType.SRT -> srtClient.sendAudio(aacBuffer, info) + ClientType.UDP -> udpClient.sendAudio(aacBuffer, info) else -> {} } } diff --git a/library/src/main/java/com/pedro/library/generic/GenericStream.kt b/library/src/main/java/com/pedro/library/generic/GenericStream.kt index 066644450..5ee53b659 100644 --- a/library/src/main/java/com/pedro/library/generic/GenericStream.kt +++ b/library/src/main/java/com/pedro/library/generic/GenericStream.kt @@ -34,9 +34,11 @@ import com.pedro.library.util.streamclient.RtmpStreamClient import com.pedro.library.util.streamclient.RtspStreamClient import com.pedro.library.util.streamclient.SrtStreamClient import com.pedro.library.util.streamclient.StreamClientListener +import com.pedro.library.util.streamclient.UdpStreamClient import com.pedro.rtmp.rtmp.RtmpClient import com.pedro.rtsp.rtsp.RtspClient import com.pedro.srt.srt.SrtClient +import com.pedro.udp.UdpClient import java.nio.ByteBuffer /** @@ -61,10 +63,12 @@ class GenericStream( private val rtmpClient = RtmpClient(connectChecker) private val rtspClient = RtspClient(connectChecker) private val srtClient = SrtClient(connectChecker) + private val udpClient = UdpClient(connectChecker) private val streamClient = GenericStreamClient( RtmpStreamClient(rtmpClient, streamClientListener), RtspStreamClient(rtspClient, streamClientListener), - SrtStreamClient(srtClient, streamClientListener) + SrtStreamClient(srtClient, streamClientListener), + UdpStreamClient(udpClient, streamClientListener) ) private var connectedType = ClientType.NONE @@ -80,6 +84,7 @@ class GenericStream( rtmpClient.setVideoCodec(codec) rtspClient.setVideoCodec(codec) srtClient.setVideoCodec(codec) + udpClient.setVideoCodec(codec) } override fun setAudioCodecImp(codec: AudioCodec) { @@ -89,12 +94,14 @@ class GenericStream( rtmpClient.setAudioCodec(codec) rtspClient.setAudioCodec(codec) srtClient.setAudioCodec(codec) + udpClient.setAudioCodec(codec) } override fun audioInfo(sampleRate: Int, isStereo: Boolean) { rtmpClient.setAudioInfo(sampleRate, isStereo) rtspClient.setAudioInfo(sampleRate, isStereo) srtClient.setAudioInfo(sampleRate, isStereo) + udpClient.setAudioInfo(sampleRate, isStereo) } override fun rtpStartStream(endPoint: String) { @@ -108,6 +115,9 @@ class GenericStream( } else if (endPoint.startsWith("srt", ignoreCase = true)) { connectedType = ClientType.SRT startStreamRtpSrt(endPoint) + } else if (endPoint.startsWith("udp", ignoreCase = true)) { + connectedType = ClientType.UDP + startStreamRtpUdp(endPoint) } else { onMainThreadHandler { connectChecker.onConnectionFailed("Unsupported protocol. Only support rtmp, rtsp and srt") @@ -130,11 +140,16 @@ class GenericStream( srtClient.connect(endPoint) } + private fun startStreamRtpUdp(endPoint: String) { + udpClient.connect(endPoint) + } + override fun rtpStopStream() { when (connectedType) { ClientType.RTMP -> rtmpClient.disconnect() ClientType.RTSP -> rtspClient.disconnect() ClientType.SRT -> srtClient.disconnect() + ClientType.UDP -> udpClient.disconnect() else -> {} } connectedType = ClientType.NONE @@ -145,6 +160,7 @@ class GenericStream( ClientType.RTMP -> rtmpClient.sendAudio(aacBuffer, info) ClientType.RTSP -> rtspClient.sendAudio(aacBuffer, info) ClientType.SRT -> srtClient.sendAudio(aacBuffer, info) + ClientType.UDP -> udpClient.sendAudio(aacBuffer, info) else -> {} } } @@ -153,6 +169,7 @@ class GenericStream( rtmpClient.setVideoInfo(sps, pps, vps) rtspClient.setVideoInfo(sps, pps, vps) srtClient.setVideoInfo(sps, pps, vps) + udpClient.setVideoInfo(sps, pps, vps) } override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { @@ -160,6 +177,7 @@ class GenericStream( ClientType.RTMP -> rtmpClient.sendVideo(h264Buffer, info) ClientType.RTSP -> rtspClient.sendVideo(h264Buffer, info) ClientType.SRT -> srtClient.sendVideo(h264Buffer, info) + ClientType.UDP -> udpClient.sendVideo(h264Buffer, info) else -> {} } } diff --git a/library/src/main/java/com/pedro/library/util/streamclient/GenericStreamClient.kt b/library/src/main/java/com/pedro/library/util/streamclient/GenericStreamClient.kt index 62525954f..e82d4c8c0 100644 --- a/library/src/main/java/com/pedro/library/util/streamclient/GenericStreamClient.kt +++ b/library/src/main/java/com/pedro/library/util/streamclient/GenericStreamClient.kt @@ -27,6 +27,7 @@ class GenericStreamClient( private val rtmpClient: RtmpStreamClient, private val rtspClient: RtspStreamClient, private val srtClient: SrtStreamClient, + private val udpClient: UdpStreamClient, ): StreamBaseClient() { private var connectedStreamClient : StreamBaseClient? = null @@ -90,6 +91,7 @@ class GenericStreamClient( rtmpClient.setReTries(reTries) rtspClient.setReTries(reTries) srtClient.setReTries(reTries) + udpClient.setReTries(reTries) } override fun reTry(delay: Long, reason: String, backupUrl: String?): Boolean { @@ -102,6 +104,7 @@ class GenericStreamClient( rtmpClient.setLogs(enabled) rtspClient.setLogs(enabled) srtClient.setLogs(enabled) + udpClient.setLogs(enabled) } override fun setCheckServerAlive(enabled: Boolean) { @@ -114,12 +117,14 @@ class GenericStreamClient( rtmpClient.resizeCache(newSize) rtspClient.resizeCache(newSize) srtClient.resizeCache(newSize) + udpClient.resizeCache(newSize) } override fun clearCache() { rtmpClient.clearCache() rtspClient.clearCache() srtClient.clearCache() + udpClient.clearCache() } override fun getCacheSize(): Int = connectedStreamClient?.getCacheSize() ?: 0 @@ -138,34 +143,42 @@ class GenericStreamClient( rtmpClient.resetSentAudioFrames() rtspClient.resetSentAudioFrames() srtClient.resetSentAudioFrames() + udpClient.resetSentAudioFrames() } override fun resetSentVideoFrames() { rtmpClient.resetSentVideoFrames() rtspClient.resetSentVideoFrames() srtClient.resetSentVideoFrames() + udpClient.resetSentVideoFrames() } override fun resetDroppedAudioFrames() { rtmpClient.resetDroppedAudioFrames() + rtspClient.resetDroppedAudioFrames() + srtClient.resetDroppedAudioFrames() + udpClient.resetDroppedAudioFrames() } override fun resetDroppedVideoFrames() { rtmpClient.resetDroppedVideoFrames() rtspClient.resetDroppedVideoFrames() srtClient.resetDroppedVideoFrames() + udpClient.resetDroppedVideoFrames() } override fun setOnlyAudio(onlyAudio: Boolean) { rtmpClient.setOnlyAudio(onlyAudio) rtspClient.setOnlyAudio(onlyAudio) srtClient.setOnlyAudio(onlyAudio) + udpClient.setOnlyAudio(onlyAudio) } override fun setOnlyVideo(onlyVideo: Boolean) { rtmpClient.setOnlyVideo(onlyVideo) rtspClient.setOnlyVideo(onlyVideo) srtClient.setOnlyVideo(onlyVideo) + udpClient.setOnlyVideo(onlyVideo) } fun connecting(url: String) { @@ -176,6 +189,8 @@ class GenericStreamClient( rtspClient } else if (url.startsWith("srt", ignoreCase = true)){ srtClient + } else if (url.startsWith("udp", ignoreCase = true)){ + udpClient } else null } } \ No newline at end of file diff --git a/library/src/main/java/com/pedro/library/util/streamclient/UdpStreamClient.kt b/library/src/main/java/com/pedro/library/util/streamclient/UdpStreamClient.kt new file mode 100644 index 000000000..68db2010d --- /dev/null +++ b/library/src/main/java/com/pedro/library/util/streamclient/UdpStreamClient.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 com.pedro.library.util.streamclient + +import com.pedro.udp.UdpClient + +/** + * Created by pedro on 12/10/23. + */ +class UdpStreamClient( + private val udpClient: UdpClient, + private val streamClientListener: StreamClientListener? +): StreamBaseClient() { + + + override fun setAuthorization(user: String?, password: String?) { + + } + + override fun setReTries(reTries: Int) { + udpClient.setReTries(reTries) + } + + override fun reTry(delay: Long, reason: String, backupUrl: String?): Boolean { + val result = udpClient.shouldRetry(reason) + if (result) { + streamClientListener?.onRequestKeyframe() + udpClient.reConnect(delay, backupUrl) + } + return result + } + + override fun hasCongestion(percentUsed: Float): Boolean = udpClient.hasCongestion(percentUsed) + + override fun setLogs(enabled: Boolean) { + udpClient.setLogs(enabled) + } + + override fun setCheckServerAlive(enabled: Boolean) { + + } + + override fun resizeCache(newSize: Int) { + udpClient.resizeCache(newSize) + } + + override fun clearCache() { + udpClient.clearCache() + } + + override fun getCacheSize(): Int = udpClient.cacheSize + + override fun getItemsInCache(): Int = udpClient.getItemsInCache() + + override fun getSentAudioFrames(): Long = udpClient.sentAudioFrames + + override fun getSentVideoFrames(): Long = udpClient.sentVideoFrames + + override fun getDroppedAudioFrames(): Long = udpClient.droppedAudioFrames + + override fun getDroppedVideoFrames(): Long = udpClient.droppedVideoFrames + + override fun resetSentAudioFrames() { + udpClient.resetSentAudioFrames() + } + + override fun resetSentVideoFrames() { + udpClient.resetSentVideoFrames() + } + + override fun resetDroppedAudioFrames() { + udpClient.resetDroppedAudioFrames() + } + + override fun resetDroppedVideoFrames() { + udpClient.resetDroppedVideoFrames() + } + + override fun setOnlyAudio(onlyAudio: Boolean) { + udpClient.setOnlyAudio(onlyAudio) + } + + override fun setOnlyVideo(onlyVideo: Boolean) { + udpClient.setOnlyVideo(onlyVideo) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 5aadddf0f..dff5e0135 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,4 +16,4 @@ dependencyResolutionManagement { } rootProject.name = "RootEncoder" -include(":app", ":rtmp", ":encoder", ":rtsp", ":library", ":srt", ":common") +include(":app", ":rtmp", ":encoder", ":rtsp", ":library", ":srt", ":udp", ":common") \ No newline at end of file diff --git a/udp/.gitignore b/udp/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/udp/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/udp/build.gradle.kts b/udp/build.gradle.kts new file mode 100644 index 000000000..9f8c73085 --- /dev/null +++ b/udp/build.gradle.kts @@ -0,0 +1,64 @@ +val libraryGroup: String by rootProject.extra +val vName: String by rootProject.extra +val coroutinesVersion: String by rootProject.extra +val junitVersion: String by rootProject.extra +val mockitoVersion: String by rootProject.extra + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("maven-publish") + id("org.jetbrains.dokka") +} + +android { + namespace = "com.pedro.udp" + compileSdk = 34 + + defaultConfig { + minSdk = 16 + lint.targetSdk = 34 + } + buildTypes { + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + + publishing { + singleVariant("release") + } +} + +afterEvaluate { + publishing { + publications { + // Creates a Maven publication called "release". + create("release") { + // Applies the component for the release build variant. + from(components["release"]) + + // You can then customize attributes of the publication as shown below. + groupId = libraryGroup + artifactId = "udp" + version = vName + } + } + } +} + +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") + testImplementation("junit:junit:$junitVersion") + testImplementation("org.mockito.kotlin:mockito-kotlin:$mockitoVersion") + implementation(project(":srt")) + api(project(":common")) +} diff --git a/udp/consumer-rules.pro b/udp/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/udp/proguard-rules.pro b/udp/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/udp/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/udp/src/main/AndroidManifest.xml b/udp/src/main/AndroidManifest.xml new file mode 100644 index 000000000..6d2092e74 --- /dev/null +++ b/udp/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/udp/src/main/java/com/pedro/udp/CommandManager.kt b/udp/src/main/java/com/pedro/udp/CommandManager.kt new file mode 100644 index 000000000..75d7e0c34 --- /dev/null +++ b/udp/src/main/java/com/pedro/udp/CommandManager.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 com.pedro.udp + +import com.pedro.common.AudioCodec +import com.pedro.common.VideoCodec +import com.pedro.srt.mpeg2ts.MpegTsPacket +import com.pedro.srt.utils.Constants +import com.pedro.udp.utils.UdpSocket +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import java.io.IOException + +/** + * Created by pedro on 6/3/24. + */ +class CommandManager { + + var MTU = Constants.MTU + var audioDisabled = false + var videoDisabled = false + var host = "" + //Avoid write a packet in middle of other. + private val writeSync = Mutex(locked = false) + var videoCodec = VideoCodec.H264 + var audioCodec = AudioCodec.AAC + + @Throws(IOException::class) + suspend fun writeData(packet: MpegTsPacket, socket: UdpSocket?): Int { + writeSync.withLock { + return socket?.write(packet) ?: 0 + } + } + + fun reset() { + MTU = Constants.MTU + host = "" + } +} \ No newline at end of file diff --git a/udp/src/main/java/com/pedro/udp/UdpClient.kt b/udp/src/main/java/com/pedro/udp/UdpClient.kt new file mode 100644 index 000000000..97e0ff139 --- /dev/null +++ b/udp/src/main/java/com/pedro/udp/UdpClient.kt @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 com.pedro.udp + +import android.media.MediaCodec +import android.util.Log +import com.pedro.common.AudioCodec +import com.pedro.common.ConnectChecker +import com.pedro.common.VideoCodec +import com.pedro.common.onMainThread +import com.pedro.udp.utils.UdpSocket +import com.pedro.udp.utils.UdpType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.nio.ByteBuffer +import java.util.regex.Pattern + +/** + * Created by pedro on 6/3/24. + */ +class UdpClient(private val connectChecker: ConnectChecker) { + + private val TAG = "UdpClient" + + private val urlPattern: Pattern = Pattern.compile("^udp://([^/:]+)(?::(\\d+))*") + + private val commandManager = CommandManager() + private val udpSender = UdpSender(connectChecker, commandManager) + private var socket: UdpSocket? = null + private var scope = CoroutineScope(Dispatchers.IO) + private var job: Job? = null + private var scopeRetry = CoroutineScope(Dispatchers.IO) + private var jobRetry: Job? = null + + @Volatile + var isStreaming = false + private set + private var url: String? = null + private var doingRetry = false + private var numRetry = 0 + private var reTries = 0 + + val droppedAudioFrames: Long + get() = udpSender.droppedAudioFrames + val droppedVideoFrames: Long + get() = udpSender.droppedVideoFrames + + val cacheSize: Int + get() = udpSender.getCacheSize() + val sentAudioFrames: Long + get() = udpSender.getSentAudioFrames() + val sentVideoFrames: Long + get() = udpSender.getSentVideoFrames() + + fun setVideoCodec(videoCodec: VideoCodec) { + if (!isStreaming) { + commandManager.videoCodec = when (videoCodec) { + VideoCodec.AV1 -> throw IllegalArgumentException("Unsupported codec: ${videoCodec.name}") + else -> videoCodec + } + } + } + + fun setAudioCodec(audioCodec: AudioCodec) { + if (!isStreaming) { + commandManager.audioCodec = when (audioCodec) { + AudioCodec.G711 -> throw IllegalArgumentException("Unsupported codec: ${audioCodec.name}") + else -> audioCodec + } + } + } + + /** + * Must be called before connect + */ + fun setOnlyAudio(onlyAudio: Boolean) { + commandManager.audioDisabled = false + commandManager.videoDisabled = onlyAudio + } + + /** + * Must be called before connect + */ + fun setOnlyVideo(onlyVideo: Boolean) { + commandManager.videoDisabled = false + commandManager.audioDisabled = onlyVideo + } + + fun setReTries(reTries: Int) { + numRetry = reTries + this.reTries = reTries + } + + fun shouldRetry(reason: String): Boolean { + val validReason = doingRetry && !reason.contains("Endpoint malformed") + return validReason && reTries > 0 + } + + @JvmOverloads + fun connect(url: String?, isRetry: Boolean = false) { + if (!isRetry) doingRetry = true + if (!isStreaming || isRetry) { + isStreaming = true + + job = scope.launch { + if (url == null) { + isStreaming = false + onMainThread { + connectChecker.onConnectionFailed("Endpoint malformed, should be: udp://ip:port") + } + return@launch + } + this@UdpClient.url = url + onMainThread { + connectChecker.onConnectionStarted(url) + } + val srtMatcher = urlPattern.matcher(url) + if (!srtMatcher.matches()) { + isStreaming = false + onMainThread { + connectChecker.onConnectionFailed("Endpoint malformed, should be: udp://ip:port") + } + return@launch + } + val host = srtMatcher.group(1) ?: "" + val port: Int? = srtMatcher.group(2)?.toInt() + if (port == null) { + connectChecker.onConnectionFailed("Endpoint malformed, port is required") + return@launch + } + commandManager.host = host + + val error = runCatching { + val type = UdpType.getTypeByHost(host) + socket = UdpSocket(host, type, port) + socket?.connect() + + udpSender.socket = socket + udpSender.start() + onMainThread { + connectChecker.onConnectionSuccess() + } + }.exceptionOrNull() + if (error != null) { + Log.e(TAG, "connection error", error) + onMainThread { + connectChecker.onConnectionFailed("Error configure stream, ${error.message}") + } + return@launch + } + } + } + } + + fun disconnect() { + CoroutineScope(Dispatchers.IO).launch { + disconnect(true) + } + } + + private suspend fun disconnect(clear: Boolean) { + if (isStreaming) udpSender.stop(clear) + socket?.close() + if (clear) { + reTries = numRetry + doingRetry = false + isStreaming = false + onMainThread { + connectChecker.onDisconnect() + } + jobRetry?.cancelAndJoin() + jobRetry = null + scopeRetry.cancel() + scopeRetry = CoroutineScope(Dispatchers.IO) + } + commandManager.reset() + job?.cancelAndJoin() + job = null + scope.cancel() + scope = CoroutineScope(Dispatchers.IO) + } + + fun reConnect(delay: Long) { + reConnect(delay, null) + } + + fun reConnect(delay: Long, backupUrl: String?) { + jobRetry = scopeRetry.launch { + reTries-- + disconnect(false) + delay(delay) + val reconnectUrl = backupUrl ?: url + connect(reconnectUrl, true) + } + } + + fun setAudioInfo(sampleRate: Int, isStereo: Boolean) { + udpSender.setAudioInfo(sampleRate, isStereo) + } + + fun setVideoInfo(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) { + Log.i(TAG, "send sps and pps") + udpSender.setVideoInfo(sps, pps, vps) + } + + fun sendVideo(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { + if (!commandManager.videoDisabled) { + udpSender.sendVideoFrame(h264Buffer, info) + } + } + + fun sendAudio(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) { + if (!commandManager.audioDisabled) { + udpSender.sendAudioFrame(aacBuffer, info) + } + } + + @Throws(IllegalArgumentException::class) + fun hasCongestion(): Boolean { + return hasCongestion(20f) + } + + @Throws(IllegalArgumentException::class) + fun hasCongestion(percentUsed: Float): Boolean { + return udpSender.hasCongestion(percentUsed) + } + + fun resetSentAudioFrames() { + udpSender.resetSentAudioFrames() + } + + fun resetSentVideoFrames() { + udpSender.resetSentVideoFrames() + } + + fun resetDroppedAudioFrames() { + udpSender.resetDroppedAudioFrames() + } + + fun resetDroppedVideoFrames() { + udpSender.resetDroppedVideoFrames() + } + + @Throws(RuntimeException::class) + fun resizeCache(newSize: Int) { + udpSender.resizeCache(newSize) + } + + fun setLogs(enable: Boolean) { + udpSender.setLogs(enable) + } + + fun clearCache() { + udpSender.clearCache() + } + + fun getItemsInCache(): Int = udpSender.getItemsInCache() +} \ No newline at end of file diff --git a/udp/src/main/java/com/pedro/udp/UdpSender.kt b/udp/src/main/java/com/pedro/udp/UdpSender.kt new file mode 100644 index 000000000..242a1c85d --- /dev/null +++ b/udp/src/main/java/com/pedro/udp/UdpSender.kt @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 com.pedro.udp + +import android.media.MediaCodec +import android.util.Log +import com.pedro.common.AudioCodec +import com.pedro.common.BitrateManager +import com.pedro.common.ConnectChecker +import com.pedro.common.onMainThread +import com.pedro.common.trySend +import com.pedro.srt.mpeg2ts.MpegTsPacket +import com.pedro.srt.mpeg2ts.MpegTsPacketizer +import com.pedro.srt.mpeg2ts.MpegType +import com.pedro.srt.mpeg2ts.Pid +import com.pedro.srt.mpeg2ts.packets.AacPacket +import com.pedro.srt.mpeg2ts.packets.BasePacket +import com.pedro.srt.mpeg2ts.packets.H26XPacket +import com.pedro.srt.mpeg2ts.packets.OpusPacket +import com.pedro.srt.mpeg2ts.psi.PsiManager +import com.pedro.srt.mpeg2ts.psi.TableToSend +import com.pedro.srt.mpeg2ts.service.Mpeg2TsService +import com.pedro.srt.srt.packets.SrtPacket +import com.pedro.srt.srt.packets.data.PacketPosition +import com.pedro.srt.utils.Constants +import com.pedro.srt.utils.toCodec +import com.pedro.udp.utils.UdpSocket +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible +import java.nio.ByteBuffer +import java.util.concurrent.BlockingQueue +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit + +/** + * Created by pedro on 6/3/24. + */ +class UdpSender( + private val connectChecker: ConnectChecker, + private val commandManager: CommandManager +) { + + private val service = Mpeg2TsService() + + private val psiManager = PsiManager(service).apply { + upgradePatVersion() + upgradeSdtVersion() + } + private val limitSize = Constants.MTU - SrtPacket.headerSize + private val mpegTsPacketizer = MpegTsPacketizer(psiManager) + private var audioPacket: BasePacket = AacPacket(limitSize, psiManager) + private val h26XPacket = H26XPacket(limitSize, psiManager) + + @Volatile + private var running = false + private var cacheSize = 200 + + private var job: Job? = null + private val scope = CoroutineScope(Dispatchers.IO) + @Volatile + private var queue: BlockingQueue> = LinkedBlockingQueue(cacheSize) + private var audioFramesSent: Long = 0 + private var videoFramesSent: Long = 0 + var socket: UdpSocket? = null + var droppedAudioFrames: Long = 0 + private set + var droppedVideoFrames: Long = 0 + private set + + private val bitrateManager: BitrateManager = BitrateManager(connectChecker) + private var isEnableLogs = true + + companion object { + private const val TAG = "SrtSender" + } + + private fun setTrackConfig(videoEnabled: Boolean, audioEnabled: Boolean) { + Pid.reset() + service.clearTracks() + if (audioEnabled) service.addTrack(commandManager.audioCodec.toCodec()) + if (videoEnabled) service.addTrack(commandManager.videoCodec.toCodec()) + service.generatePmt() + psiManager.updateService(service) + } + + fun setVideoInfo(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) { + h26XPacket.setVideoCodec(commandManager.videoCodec.toCodec()) + h26XPacket.sendVideoInfo(sps, pps, vps) + } + + fun setAudioInfo(sampleRate: Int, isStereo: Boolean) { + when (commandManager.audioCodec) { + AudioCodec.AAC -> { + audioPacket = AacPacket(limitSize, psiManager) + (audioPacket as? AacPacket)?.sendAudioInfo(sampleRate, isStereo) + } + AudioCodec.OPUS -> { + audioPacket = OpusPacket(limitSize, psiManager) + } + AudioCodec.G711 -> { + throw IllegalArgumentException("Unsupported codec: ${commandManager.audioCodec.name}") + } + } + } + + fun sendVideoFrame(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { + if (running) { + h26XPacket.createAndSendPacket(h264Buffer, info) { mpegTsPackets -> + val isKey = mpegTsPackets[0].isKey + checkSendInfo(isKey) + val result = queue.trySend(mpegTsPackets) + if (!result) { + Log.i(TAG, "Video frame discarded") + droppedVideoFrames++ + } + } + } + } + + fun sendAudioFrame(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) { + if (running) { + audioPacket.createAndSendPacket(aacBuffer, info) { mpegTsPackets -> + val isKey = mpegTsPackets[0].isKey + checkSendInfo(isKey) + val result = queue.trySend(mpegTsPackets) + if (!result) { + Log.i(TAG, "Audio frame discarded") + droppedAudioFrames++ + } + } + } + } + + fun start() { + queue.clear() + setTrackConfig(!commandManager.videoDisabled, !commandManager.audioDisabled) + running = true + job = scope.launch { + //send config + val psiPackets = mpegTsPacketizer.write(listOf(psiManager.getPmt(), psiManager.getSdt(), psiManager.getPat())).map { b -> + MpegTsPacket(b, MpegType.PSI, PacketPosition.SINGLE, isKey = false) + } + queue.trySend(psiPackets) + var bytesSend = 0L + val bitrateTask = async { + while (scope.isActive && running) { + //bytes to bits + bitrateManager.calculateBitrate(bytesSend * 8) + bytesSend = 0 + delay(timeMillis = 1000) + } + } + while (scope.isActive && running) { + val error = runCatching { + val mpegTsPackets = runInterruptible { + queue.poll(1, TimeUnit.SECONDS) + } + mpegTsPackets.forEach { mpegTsPacket -> + var size = 0 + size += commandManager.writeData(mpegTsPacket, socket) + if (isEnableLogs) { + Log.i(TAG, "wrote ${mpegTsPacket.type.name} packet, size $size") + } + bytesSend += size + } + }.exceptionOrNull() + if (error != null) { + onMainThread { + connectChecker.onConnectionFailed("Error send packet, " + error.message) + } + Log.e(TAG, "send error: ", error) + return@launch + } + } + } + } + + private fun checkSendInfo(isKey: Boolean = false) { + when (psiManager.shouldSend(isKey)) { + TableToSend.PAT_PMT -> { + val psiPackets = mpegTsPacketizer.write(listOf(psiManager.getPat(), psiManager.getPmt()), increasePsiContinuity = true).map { b -> + MpegTsPacket(b, MpegType.PSI, PacketPosition.SINGLE, isKey = false) + } + queue.trySend(psiPackets) + } + TableToSend.SDT -> { + val psiPackets = mpegTsPacketizer.write(listOf(psiManager.getSdt()), increasePsiContinuity = true).map { b -> + MpegTsPacket(b, MpegType.PSI, PacketPosition.SINGLE, isKey = false) + } + queue.trySend(psiPackets) + } + TableToSend.NONE -> {} + TableToSend.ALL -> { + val psiPackets = mpegTsPacketizer.write(listOf(psiManager.getPmt(), psiManager.getSdt(), psiManager.getPat()), increasePsiContinuity = true).map { b -> + MpegTsPacket(b, MpegType.PSI, PacketPosition.SINGLE, isKey = false) + } + queue.trySend(psiPackets) + } + } + } + + suspend fun stop(clear: Boolean) { + running = false + psiManager.reset() + service.clear() + mpegTsPacketizer.reset() + audioPacket.reset(clear) + h26XPacket.reset(clear) + resetSentAudioFrames() + resetSentVideoFrames() + resetDroppedAudioFrames() + resetDroppedVideoFrames() + job?.cancelAndJoin() + job = null + queue.clear() + } + + @Throws(IllegalArgumentException::class) + fun hasCongestion(percentUsed: Float = 20f): Boolean { + if (percentUsed < 0 || percentUsed > 100) throw IllegalArgumentException("the value must be in range 0 to 100") + val size = queue.size.toFloat() + val remaining = queue.remainingCapacity().toFloat() + val capacity = size + remaining + return size >= capacity * (percentUsed / 100f) + } + + fun resizeCache(newSize: Int) { + if (newSize < queue.size - queue.remainingCapacity()) { + throw RuntimeException("Can't fit current cache inside new cache size") + } + val tempQueue: BlockingQueue> = LinkedBlockingQueue(newSize) + queue.drainTo(tempQueue) + queue = tempQueue + } + + fun getCacheSize(): Int { + return cacheSize + } + + fun getItemsInCache(): Int = queue.size + + fun clearCache() { + queue.clear() + } + + fun getSentAudioFrames(): Long { + return audioFramesSent + } + + fun getSentVideoFrames(): Long { + return videoFramesSent + } + + fun resetSentAudioFrames() { + audioFramesSent = 0 + } + + fun resetSentVideoFrames() { + videoFramesSent = 0 + } + + fun resetDroppedAudioFrames() { + droppedAudioFrames = 0 + } + + fun resetDroppedVideoFrames() { + droppedVideoFrames = 0 + } + + fun setLogs(enable: Boolean) { + isEnableLogs = enable + } +} \ No newline at end of file diff --git a/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt b/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt new file mode 100644 index 000000000..014311042 --- /dev/null +++ b/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 com.pedro.udp.utils + +import com.pedro.srt.mpeg2ts.MpegTsPacket +import com.pedro.srt.utils.Constants +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.InetAddress +import java.net.MulticastSocket + +/** + * Created by pedro on 6/3/24. + */ +class UdpSocket(private val host: String, private val type: UdpType, private val port: Int) { + + private var socket: DatagramSocket? = null + private var packetSize = Constants.MTU + private val timeout = 5000 + + fun connect() { + val address = InetAddress.getByName(host) + socket = when (type) { + UdpType.UNICAST -> DatagramSocket() + UdpType.MULTICAST -> MulticastSocket() + UdpType.BROADCAST -> DatagramSocket().apply { broadcast = true } + } + socket = DatagramSocket() + socket?.connect(address, port) + socket?.soTimeout = timeout + } + + fun close() { + if (socket?.isClosed == false) { + socket?.disconnect() + socket?.close() + socket = null + } + } + + fun isConnected(): Boolean { + return socket?.isConnected ?: false + } + + fun isReachable(): Boolean { + return socket?.inetAddress?.isReachable(5000) ?: false + } + + fun setPacketSize(size: Int) { + packetSize = size + } + + fun write(mpegTsPacket: MpegTsPacket): Int { + val buffer = mpegTsPacket.buffer + val udpPacket = DatagramPacket(buffer, buffer.size) + socket?.send(udpPacket) + return buffer.size + } +} \ No newline at end of file diff --git a/udp/src/main/java/com/pedro/udp/utils/UdpType.kt b/udp/src/main/java/com/pedro/udp/utils/UdpType.kt new file mode 100644 index 000000000..2bf17fcd2 --- /dev/null +++ b/udp/src/main/java/com/pedro/udp/utils/UdpType.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 com.pedro.udp.utils + +/** + * Created by pedro on 6/3/24. + */ +enum class UdpType { + UNICAST, MULTICAST, BROADCAST; + + companion object { + fun getTypeByHost(host: String): UdpType { + return when { + host.startsWith("224.") -> MULTICAST + host.startsWith("255.") -> BROADCAST + else -> UNICAST + } + } + } +} \ No newline at end of file diff --git a/udp/src/test/java/android/util/Log.java b/udp/src/test/java/android/util/Log.java new file mode 100644 index 000000000..4831e9d2d --- /dev/null +++ b/udp/src/test/java/android/util/Log.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 android.util; + +/** + * Created by pedro on 16/6/21. + * + * Replace Log class of Android for testing purpose + */ +public class Log { + + public static int i(String tag, String message, Throwable throwable) { + return println(tag, message, throwable); + } + + public static int i(String tag, String message) { + return println(tag, message, null); + } + + public static int e(String tag, String message, Throwable throwable) { + return printlnError(tag, message, throwable); + } + + public static int e(String tag, String message) { + return printlnError(tag, message, null); + } + + private static int println(String tag, String message, Throwable throwable) { + System.out.println(tag + ": " + message); + if (throwable != null) throwable.printStackTrace(); + return 0; + } + + private static int printlnError(String tag, String message, Throwable throwable) { + System.err.println(tag + ": " + message); + if (throwable != null) throwable.printStackTrace(); + return 0; + } +} diff --git a/udp/src/test/java/com/pedro/udp/ExampleUnitTest.kt b/udp/src/test/java/com/pedro/udp/ExampleUnitTest.kt new file mode 100644 index 000000000..e99a2e3ea --- /dev/null +++ b/udp/src/test/java/com/pedro/udp/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.pedro.udp + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file From 4e848f2fc359cb7d005092674e3243497c3911a8 Mon Sep 17 00:00:00 2001 From: pedro Date: Wed, 6 Mar 2024 13:18:39 +0100 Subject: [PATCH 2/7] fix fail callback on main thread --- README.md | 2 +- udp/src/main/java/com/pedro/udp/UdpClient.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fd0a590d2..9cb12cf81 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ dependencies { - [X] Get upload bandwidth used. - [X] H264, H265, AAC and OPUS support. - [X] Unicast, Multicast and Broadcast support. -- [X] MPEG-TS support. +- [X] MPEG2-TS support. https://haivision.github.io/srt-rfc/draft-sharabayko-srt.html diff --git a/udp/src/main/java/com/pedro/udp/UdpClient.kt b/udp/src/main/java/com/pedro/udp/UdpClient.kt index 97e0ff139..214c280c2 100644 --- a/udp/src/main/java/com/pedro/udp/UdpClient.kt +++ b/udp/src/main/java/com/pedro/udp/UdpClient.kt @@ -144,7 +144,9 @@ class UdpClient(private val connectChecker: ConnectChecker) { val host = srtMatcher.group(1) ?: "" val port: Int? = srtMatcher.group(2)?.toInt() if (port == null) { - connectChecker.onConnectionFailed("Endpoint malformed, port is required") + onMainThread { + connectChecker.onConnectionFailed("Endpoint malformed, port is required") + } return@launch } commandManager.host = host From 1f02b41629cb6aa7d285c3540f5f87f0bee07957 Mon Sep 17 00:00:00 2001 From: pedro Date: Wed, 6 Mar 2024 13:24:43 +0100 Subject: [PATCH 3/7] fix getTypeByHost --- udp/src/main/java/com/pedro/udp/utils/UdpType.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/udp/src/main/java/com/pedro/udp/utils/UdpType.kt b/udp/src/main/java/com/pedro/udp/utils/UdpType.kt index 2bf17fcd2..ac3f7f212 100644 --- a/udp/src/main/java/com/pedro/udp/utils/UdpType.kt +++ b/udp/src/main/java/com/pedro/udp/utils/UdpType.kt @@ -24,9 +24,10 @@ enum class UdpType { companion object { fun getTypeByHost(host: String): UdpType { - return when { - host.startsWith("224.") -> MULTICAST - host.startsWith("255.") -> BROADCAST + val firstNumber = host.split(".")[0].toIntOrNull() + return when (firstNumber) { + in 224..239 -> MULTICAST + 255 -> BROADCAST else -> UNICAST } } From 873b348dea875816cad03417671df8b4a376e836 Mon Sep 17 00:00:00 2001 From: pedro Date: Wed, 6 Mar 2024 13:30:50 +0100 Subject: [PATCH 4/7] fix pattern to accept / as last character --- udp/src/main/java/com/pedro/udp/UdpClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/udp/src/main/java/com/pedro/udp/UdpClient.kt b/udp/src/main/java/com/pedro/udp/UdpClient.kt index 214c280c2..524d30b2b 100644 --- a/udp/src/main/java/com/pedro/udp/UdpClient.kt +++ b/udp/src/main/java/com/pedro/udp/UdpClient.kt @@ -41,7 +41,7 @@ class UdpClient(private val connectChecker: ConnectChecker) { private val TAG = "UdpClient" - private val urlPattern: Pattern = Pattern.compile("^udp://([^/:]+)(?::(\\d+))*") + private val urlPattern: Pattern = Pattern.compile("^udp://([^/:]+)(?::(\\d+))*/?") private val commandManager = CommandManager() private val udpSender = UdpSender(connectChecker, commandManager) From 5446a06951f9b9250e8163ddce6cb01b670fd386 Mon Sep 17 00:00:00 2001 From: pedro Date: Wed, 6 Mar 2024 13:47:55 +0100 Subject: [PATCH 5/7] fix udpsocket --- udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt b/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt index 014311042..2ddf9e6ad 100644 --- a/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt +++ b/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt @@ -36,10 +36,9 @@ class UdpSocket(private val host: String, private val type: UdpType, private val val address = InetAddress.getByName(host) socket = when (type) { UdpType.UNICAST -> DatagramSocket() - UdpType.MULTICAST -> MulticastSocket() + UdpType.MULTICAST -> MulticastSocket().apply { timeToLive = 13 } UdpType.BROADCAST -> DatagramSocket().apply { broadcast = true } } - socket = DatagramSocket() socket?.connect(address, port) socket?.soTimeout = timeout } From b021b3cb9deb32e36e8fc1544811a4419f834f06 Mon Sep 17 00:00:00 2001 From: pedro Date: Wed, 6 Mar 2024 16:36:52 +0100 Subject: [PATCH 6/7] fix possible npe --- .../java/com/pedro/srt/mpeg2ts/psi/PsiManager.kt | 3 +-- srt/src/main/java/com/pedro/srt/srt/SrtSender.kt | 9 ++++++--- udp/src/main/java/com/pedro/udp/UdpSender.kt | 12 +++++++----- udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt | 13 ++++++++++--- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/srt/src/main/java/com/pedro/srt/mpeg2ts/psi/PsiManager.kt b/srt/src/main/java/com/pedro/srt/mpeg2ts/psi/PsiManager.kt index 153de8869..fc22bba52 100644 --- a/srt/src/main/java/com/pedro/srt/mpeg2ts/psi/PsiManager.kt +++ b/srt/src/main/java/com/pedro/srt/mpeg2ts/psi/PsiManager.kt @@ -16,7 +16,6 @@ package com.pedro.srt.mpeg2ts.psi -import com.pedro.srt.mpeg2ts.Codec import com.pedro.srt.mpeg2ts.service.Mpeg2TsService import kotlin.random.Random @@ -83,7 +82,7 @@ class PsiManager( fun getSdt(): Sdt = sdt fun getPat(): Pat = pat - fun getPmt(): Pmt = service.pmt!! + fun getPmt(): Pmt? = service.pmt fun reset() { sdtCount = 0 diff --git a/srt/src/main/java/com/pedro/srt/srt/SrtSender.kt b/srt/src/main/java/com/pedro/srt/srt/SrtSender.kt index 62811d5d4..994368915 100644 --- a/srt/src/main/java/com/pedro/srt/srt/SrtSender.kt +++ b/srt/src/main/java/com/pedro/srt/srt/SrtSender.kt @@ -158,7 +158,9 @@ class SrtSender( running = true job = scope.launch { //send config - val psiPackets = mpegTsPacketizer.write(listOf(psiManager.getPmt(), psiManager.getSdt(), psiManager.getPat())).map { b -> + val psiList = mutableListOf(psiManager.getSdt(), psiManager.getPat()) + psiManager.getPmt()?.let { psiList.add(0, it) } + val psiPackets = mpegTsPacketizer.write(psiList).map { b -> MpegTsPacket(b, MpegType.PSI, PacketPosition.SINGLE, isKey = false) } queue.trySend(psiPackets) @@ -197,9 +199,10 @@ class SrtSender( } private fun checkSendInfo(isKey: Boolean = false) { + val pmt = psiManager.getPmt() ?: return when (psiManager.shouldSend(isKey)) { TableToSend.PAT_PMT -> { - val psiPackets = mpegTsPacketizer.write(listOf(psiManager.getPat(), psiManager.getPmt()), increasePsiContinuity = true).map { b -> + val psiPackets = mpegTsPacketizer.write(listOf(psiManager.getPat(), pmt), increasePsiContinuity = true).map { b -> MpegTsPacket(b, MpegType.PSI, PacketPosition.SINGLE, isKey = false) } queue.trySend(psiPackets) @@ -212,7 +215,7 @@ class SrtSender( } TableToSend.NONE -> {} TableToSend.ALL -> { - val psiPackets = mpegTsPacketizer.write(listOf(psiManager.getPmt(), psiManager.getSdt(), psiManager.getPat()), increasePsiContinuity = true).map { b -> + val psiPackets = mpegTsPacketizer.write(listOf(pmt, psiManager.getSdt(), psiManager.getPat()), increasePsiContinuity = true).map { b -> MpegTsPacket(b, MpegType.PSI, PacketPosition.SINGLE, isKey = false) } queue.trySend(psiPackets) diff --git a/udp/src/main/java/com/pedro/udp/UdpSender.kt b/udp/src/main/java/com/pedro/udp/UdpSender.kt index 242a1c85d..490d47e91 100644 --- a/udp/src/main/java/com/pedro/udp/UdpSender.kt +++ b/udp/src/main/java/com/pedro/udp/UdpSender.kt @@ -34,7 +34,6 @@ import com.pedro.srt.mpeg2ts.packets.OpusPacket import com.pedro.srt.mpeg2ts.psi.PsiManager import com.pedro.srt.mpeg2ts.psi.TableToSend import com.pedro.srt.mpeg2ts.service.Mpeg2TsService -import com.pedro.srt.srt.packets.SrtPacket import com.pedro.srt.srt.packets.data.PacketPosition import com.pedro.srt.utils.Constants import com.pedro.srt.utils.toCodec @@ -67,7 +66,7 @@ class UdpSender( upgradePatVersion() upgradeSdtVersion() } - private val limitSize = Constants.MTU - SrtPacket.headerSize + private val limitSize = Constants.MTU private val mpegTsPacketizer = MpegTsPacketizer(psiManager) private var audioPacket: BasePacket = AacPacket(limitSize, psiManager) private val h26XPacket = H26XPacket(limitSize, psiManager) @@ -158,7 +157,9 @@ class UdpSender( running = true job = scope.launch { //send config - val psiPackets = mpegTsPacketizer.write(listOf(psiManager.getPmt(), psiManager.getSdt(), psiManager.getPat())).map { b -> + val psiList = mutableListOf(psiManager.getSdt(), psiManager.getPat()) + psiManager.getPmt()?.let { psiList.add(0, it) } + val psiPackets = mpegTsPacketizer.write(psiList).map { b -> MpegTsPacket(b, MpegType.PSI, PacketPosition.SINGLE, isKey = false) } queue.trySend(psiPackets) @@ -197,9 +198,10 @@ class UdpSender( } private fun checkSendInfo(isKey: Boolean = false) { + val pmt = psiManager.getPmt() ?: return when (psiManager.shouldSend(isKey)) { TableToSend.PAT_PMT -> { - val psiPackets = mpegTsPacketizer.write(listOf(psiManager.getPat(), psiManager.getPmt()), increasePsiContinuity = true).map { b -> + val psiPackets = mpegTsPacketizer.write(listOf(psiManager.getPat(), pmt), increasePsiContinuity = true).map { b -> MpegTsPacket(b, MpegType.PSI, PacketPosition.SINGLE, isKey = false) } queue.trySend(psiPackets) @@ -212,7 +214,7 @@ class UdpSender( } TableToSend.NONE -> {} TableToSend.ALL -> { - val psiPackets = mpegTsPacketizer.write(listOf(psiManager.getPmt(), psiManager.getSdt(), psiManager.getPat()), increasePsiContinuity = true).map { b -> + val psiPackets = mpegTsPacketizer.write(listOf(pmt, psiManager.getSdt(), psiManager.getPat()), increasePsiContinuity = true).map { b -> MpegTsPacket(b, MpegType.PSI, PacketPosition.SINGLE, isKey = false) } queue.trySend(psiPackets) diff --git a/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt b/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt index 2ddf9e6ad..65c5ada9a 100644 --- a/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt +++ b/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt @@ -17,7 +17,7 @@ package com.pedro.udp.utils import com.pedro.srt.mpeg2ts.MpegTsPacket -import com.pedro.srt.utils.Constants +import com.pedro.srt.mpeg2ts.MpegTsPacketizer import java.net.DatagramPacket import java.net.DatagramSocket import java.net.InetAddress @@ -29,14 +29,14 @@ import java.net.MulticastSocket class UdpSocket(private val host: String, private val type: UdpType, private val port: Int) { private var socket: DatagramSocket? = null - private var packetSize = Constants.MTU + private var packetSize = MpegTsPacketizer.packetSize private val timeout = 5000 fun connect() { val address = InetAddress.getByName(host) socket = when (type) { UdpType.UNICAST -> DatagramSocket() - UdpType.MULTICAST -> MulticastSocket().apply { timeToLive = 13 } + UdpType.MULTICAST -> MulticastSocket() UdpType.BROADCAST -> DatagramSocket().apply { broadcast = true } } socket?.connect(address, port) @@ -69,4 +69,11 @@ class UdpSocket(private val host: String, private val type: UdpType, private val socket?.send(udpPacket) return buffer.size } + + fun readBuffer(): ByteArray { + val buffer = ByteArray(packetSize) + val udpPacket = DatagramPacket(buffer, buffer.size) + socket?.receive(udpPacket) + return udpPacket.data.sliceArray(0 until udpPacket.length) + } } \ No newline at end of file From 6c149a50ce563b89fe77aed476f368eabaa07376 Mon Sep 17 00:00:00 2001 From: pedro Date: Wed, 6 Mar 2024 20:42:10 +0100 Subject: [PATCH 7/7] add Udp stream classes --- .../com/pedro/library/srt/SrtCamera1.java | 1 - .../java/com/pedro/library/udp/UdpCamera1.kt | 106 +++++++++++++++++ .../java/com/pedro/library/udp/UdpCamera2.kt | 96 +++++++++++++++ .../java/com/pedro/library/udp/UdpDisplay.kt | 82 +++++++++++++ .../java/com/pedro/library/udp/UdpFromFile.kt | 110 ++++++++++++++++++ .../com/pedro/library/udp/UdpOnlyAudio.kt | 58 +++++++++ .../java/com/pedro/library/udp/UdpStream.kt | 93 +++++++++++++++ .../util/streamclient/UdpStreamClient.kt | 2 +- 8 files changed, 546 insertions(+), 2 deletions(-) create mode 100644 library/src/main/java/com/pedro/library/udp/UdpCamera1.kt create mode 100644 library/src/main/java/com/pedro/library/udp/UdpCamera2.kt create mode 100644 library/src/main/java/com/pedro/library/udp/UdpDisplay.kt create mode 100644 library/src/main/java/com/pedro/library/udp/UdpFromFile.kt create mode 100644 library/src/main/java/com/pedro/library/udp/UdpOnlyAudio.kt create mode 100644 library/src/main/java/com/pedro/library/udp/UdpStream.kt diff --git a/library/src/main/java/com/pedro/library/srt/SrtCamera1.java b/library/src/main/java/com/pedro/library/srt/SrtCamera1.java index f6f743ecd..0ff94995f 100644 --- a/library/src/main/java/com/pedro/library/srt/SrtCamera1.java +++ b/library/src/main/java/com/pedro/library/srt/SrtCamera1.java @@ -97,7 +97,6 @@ protected void prepareAudioRtp(boolean isStereo, int sampleRate) { @Override protected void startStreamRtp(String url) { srtClient.connect(url); - requestKeyFrame(); } @Override diff --git a/library/src/main/java/com/pedro/library/udp/UdpCamera1.kt b/library/src/main/java/com/pedro/library/udp/UdpCamera1.kt new file mode 100644 index 000000000..7438c82a0 --- /dev/null +++ b/library/src/main/java/com/pedro/library/udp/UdpCamera1.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 com.pedro.library.udp + +import android.content.Context +import android.media.MediaCodec +import android.os.Build +import android.view.SurfaceView +import android.view.TextureView +import androidx.annotation.RequiresApi +import com.pedro.common.AudioCodec +import com.pedro.common.ConnectChecker +import com.pedro.common.VideoCodec +import com.pedro.library.base.Camera1Base +import com.pedro.library.util.streamclient.StreamClientListener +import com.pedro.library.util.streamclient.UdpStreamClient +import com.pedro.library.view.OpenGlView +import com.pedro.udp.UdpClient +import java.nio.ByteBuffer + +/** + * More documentation see: + * [Camera1Base] + * + * Created by pedro on 6/3/24. + */ +class UdpCamera1: Camera1Base { + + private val streamClientListener = object: StreamClientListener { + override fun onRequestKeyframe() { + requestKeyFrame() + } + } + private lateinit var udpClient: UdpClient + private lateinit var streamClient: UdpStreamClient + + constructor(surfaceView: SurfaceView, connectChecker: ConnectChecker): super(surfaceView) { + init(connectChecker) + } + + constructor(textureView: TextureView, connectChecker: ConnectChecker): super(textureView) { + init(connectChecker) + } + + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + constructor(openGlView: OpenGlView, connectChecker: ConnectChecker): super(openGlView) { + init(connectChecker) + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) + constructor(context: Context, connectChecker: ConnectChecker): super(context) { + init(connectChecker) + } + + private fun init(connectChecker: ConnectChecker) { + udpClient = UdpClient(connectChecker) + streamClient = UdpStreamClient(udpClient, streamClientListener) + } + + override fun getStreamClient(): UdpStreamClient = streamClient + + override fun setVideoCodecImp(codec: VideoCodec) { + udpClient.setVideoCodec(codec) + } + + override fun setAudioCodecImp(codec: AudioCodec) { + udpClient.setAudioCodec(codec) + } + + override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) { + udpClient.setAudioInfo(sampleRate, isStereo) + } + + override fun startStreamRtp(url: String) { + udpClient.connect(url) + } + + override fun stopStreamRtp() { + udpClient.disconnect() + } + + override fun getAacDataRtp(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) { + udpClient.sendAudio(aacBuffer, info) + } + + override fun onSpsPpsVpsRtp(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) { + udpClient.setVideoInfo(sps, pps, vps) + } + + override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { + udpClient.sendVideo(h264Buffer, info) + } +} diff --git a/library/src/main/java/com/pedro/library/udp/UdpCamera2.kt b/library/src/main/java/com/pedro/library/udp/UdpCamera2.kt new file mode 100644 index 000000000..ca0c05738 --- /dev/null +++ b/library/src/main/java/com/pedro/library/udp/UdpCamera2.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 com.pedro.library.udp + +import android.content.Context +import android.media.MediaCodec +import android.os.Build +import androidx.annotation.RequiresApi +import com.pedro.common.AudioCodec +import com.pedro.common.ConnectChecker +import com.pedro.common.VideoCodec +import com.pedro.library.base.Camera2Base +import com.pedro.library.util.streamclient.StreamClientListener +import com.pedro.library.util.streamclient.UdpStreamClient +import com.pedro.library.view.OpenGlView +import com.pedro.udp.UdpClient +import java.nio.ByteBuffer + +/** + * More documentation see: + * [Camera2Base] + * + * Created by pedro on 6/3/24. + */ +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +class UdpCamera2: Camera2Base { + + private val streamClientListener = object: StreamClientListener { + override fun onRequestKeyframe() { + requestKeyFrame() + } + } + private lateinit var udpClient: UdpClient + private lateinit var streamClient: UdpStreamClient + + constructor(openGlView: OpenGlView, connectChecker: ConnectChecker): super(openGlView) { + init(connectChecker) + } + + constructor(context: Context, useOpengl: Boolean, connectChecker: ConnectChecker): super( + context, useOpengl) { + init(connectChecker) + } + + private fun init(connectChecker: ConnectChecker) { + udpClient = UdpClient(connectChecker) + streamClient = UdpStreamClient(udpClient, streamClientListener) + } + + override fun getStreamClient(): UdpStreamClient = streamClient + + override fun setVideoCodecImp(codec: VideoCodec) { + udpClient.setVideoCodec(codec) + } + + override fun setAudioCodecImp(codec: AudioCodec) { + udpClient.setAudioCodec(codec) + } + + override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) { + udpClient.setAudioInfo(sampleRate, isStereo) + } + + override fun startStreamRtp(url: String) { + udpClient.connect(url) + } + + override fun stopStreamRtp() { + udpClient.disconnect() + } + + override fun getAacDataRtp(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) { + udpClient.sendAudio(aacBuffer, info) + } + + override fun onSpsPpsVpsRtp(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) { + udpClient.setVideoInfo(sps, pps, vps) + } + + override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { + udpClient.sendVideo(h264Buffer, info) + } +} diff --git a/library/src/main/java/com/pedro/library/udp/UdpDisplay.kt b/library/src/main/java/com/pedro/library/udp/UdpDisplay.kt new file mode 100644 index 000000000..50bc56586 --- /dev/null +++ b/library/src/main/java/com/pedro/library/udp/UdpDisplay.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 com.pedro.library.udp + +import android.content.Context +import android.media.MediaCodec +import android.os.Build +import androidx.annotation.RequiresApi +import com.pedro.common.AudioCodec +import com.pedro.common.ConnectChecker +import com.pedro.common.VideoCodec +import com.pedro.library.base.DisplayBase +import com.pedro.library.util.streamclient.StreamClientListener +import com.pedro.library.util.streamclient.UdpStreamClient +import com.pedro.udp.UdpClient +import java.nio.ByteBuffer + +/** + * More documentation see: + * [DisplayBase] + * + * Created by pedro on 6/3/24. + */ +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +class UdpDisplay(context: Context, useOpengl: Boolean, connectChecker: ConnectChecker): + DisplayBase(context, useOpengl) { + + private val streamClientListener = object: StreamClientListener { + override fun onRequestKeyframe() { + requestKeyFrame() + } + } + private val udpClient = UdpClient(connectChecker) + private val streamClient = UdpStreamClient(udpClient, streamClientListener) + + override fun setVideoCodecImp(codec: VideoCodec) { + udpClient.setVideoCodec(codec) + } + + override fun setAudioCodecImp(codec: AudioCodec) { + udpClient.setAudioCodec(codec) + } + + override fun getStreamClient(): UdpStreamClient = streamClient + + override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) { + udpClient.setAudioInfo(sampleRate, isStereo) + } + + override fun startStreamRtp(url: String) { + udpClient.connect(url) + } + + override fun stopStreamRtp() { + udpClient.disconnect() + } + + override fun getAacDataRtp(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) { + udpClient.sendAudio(aacBuffer, info) + } + + override fun onSpsPpsVpsRtp(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) { + udpClient.setVideoInfo(sps, pps, vps) + } + + override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { + udpClient.sendVideo(h264Buffer, info) + } +} diff --git a/library/src/main/java/com/pedro/library/udp/UdpFromFile.kt b/library/src/main/java/com/pedro/library/udp/UdpFromFile.kt new file mode 100644 index 000000000..512ae4086 --- /dev/null +++ b/library/src/main/java/com/pedro/library/udp/UdpFromFile.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 com.pedro.library.udp + +import android.content.Context +import android.media.MediaCodec +import android.os.Build +import androidx.annotation.RequiresApi +import com.pedro.common.AudioCodec +import com.pedro.common.ConnectChecker +import com.pedro.common.VideoCodec +import com.pedro.encoder.input.decoder.AudioDecoderInterface +import com.pedro.encoder.input.decoder.VideoDecoderInterface +import com.pedro.library.base.FromFileBase +import com.pedro.library.util.streamclient.StreamClientListener +import com.pedro.library.util.streamclient.UdpStreamClient +import com.pedro.library.view.OpenGlView +import com.pedro.udp.UdpClient +import java.nio.ByteBuffer + +/** + * More documentation see: + * [FromFileBase] + * + * Created by pedro on 6/3/24. + */ +@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) +class UdpFromFile: FromFileBase { + + private val streamClientListener = object: StreamClientListener { + override fun onRequestKeyframe() { + requestKeyFrame() + } + } + private lateinit var udpClient: UdpClient + private lateinit var streamClient: UdpStreamClient + + constructor( + openGlView: OpenGlView, connectChecker: ConnectChecker, + videoDecoderInterface: VideoDecoderInterface, audioDecoderInterface: AudioDecoderInterface + ): super(openGlView, videoDecoderInterface, audioDecoderInterface) { + init(connectChecker) + } + + constructor( + context: Context, connectChecker: ConnectChecker, + videoDecoderInterface: VideoDecoderInterface, audioDecoderInterface: AudioDecoderInterface + ): super(context, videoDecoderInterface, audioDecoderInterface) { + init(connectChecker) + } + + constructor( + connectChecker: ConnectChecker, + videoDecoderInterface: VideoDecoderInterface, audioDecoderInterface: AudioDecoderInterface + ): super(videoDecoderInterface, audioDecoderInterface) { + init(connectChecker) + } + + private fun init(connectChecker: ConnectChecker) { + udpClient = UdpClient(connectChecker) + streamClient = UdpStreamClient(udpClient, streamClientListener) + } + + override fun setVideoCodecImp(codec: VideoCodec) { + udpClient.setVideoCodec(codec) + } + + override fun setAudioCodecImp(codec: AudioCodec) { + udpClient.setAudioCodec(codec) + } + + override fun getStreamClient(): UdpStreamClient = streamClient + + override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) { + udpClient.setAudioInfo(sampleRate, isStereo) + } + + override fun startStreamRtp(url: String) { + udpClient.connect(url) + } + + override fun stopStreamRtp() { + udpClient.disconnect() + } + + override fun onSpsPpsVpsRtp(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) { + udpClient.setVideoInfo(sps, pps, vps) + } + + override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { + udpClient.sendVideo(h264Buffer, info) + } + + override fun getAacDataRtp(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) { + udpClient.sendAudio(aacBuffer, info) + } +} diff --git a/library/src/main/java/com/pedro/library/udp/UdpOnlyAudio.kt b/library/src/main/java/com/pedro/library/udp/UdpOnlyAudio.kt new file mode 100644 index 000000000..54a7ccc8f --- /dev/null +++ b/library/src/main/java/com/pedro/library/udp/UdpOnlyAudio.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 com.pedro.library.udp + +import android.media.MediaCodec +import com.pedro.common.AudioCodec +import com.pedro.common.ConnectChecker +import com.pedro.library.base.OnlyAudioBase +import com.pedro.library.util.streamclient.UdpStreamClient +import com.pedro.udp.UdpClient +import java.nio.ByteBuffer + +/** + * More documentation see: + * [OnlyAudioBase] + * + * Created by pedro on 6/3/24. + */ +class UdpOnlyAudio(connectChecker: ConnectChecker) : OnlyAudioBase() { + + private val udpClient = UdpClient(connectChecker).apply { setOnlyAudio(true) } + private val streamClient = UdpStreamClient(udpClient, null) + + override fun getStreamClient(): UdpStreamClient = streamClient + + override fun setAudioCodecImp(codec: AudioCodec) { + udpClient.setAudioCodec(codec) + } + + override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) { + udpClient.setAudioInfo(sampleRate, isStereo) + } + + override fun startStreamRtp(url: String) { + udpClient.connect(url) + } + + override fun stopStreamRtp() { + udpClient.disconnect() + } + + override fun getAacDataRtp(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) { + udpClient.sendAudio(aacBuffer, info) + } +} diff --git a/library/src/main/java/com/pedro/library/udp/UdpStream.kt b/library/src/main/java/com/pedro/library/udp/UdpStream.kt new file mode 100644 index 000000000..14dad426e --- /dev/null +++ b/library/src/main/java/com/pedro/library/udp/UdpStream.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 pedroSG94. + * + * 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 com.pedro.library.udp + +import android.content.Context +import android.media.MediaCodec +import android.os.Build +import androidx.annotation.RequiresApi +import com.pedro.common.AudioCodec +import com.pedro.common.ConnectChecker +import com.pedro.common.VideoCodec +import com.pedro.library.base.StreamBase +import com.pedro.library.util.sources.audio.AudioSource +import com.pedro.library.util.sources.audio.MicrophoneSource +import com.pedro.library.util.sources.video.Camera2Source +import com.pedro.library.util.sources.video.VideoSource +import com.pedro.library.util.streamclient.StreamClientListener +import com.pedro.library.util.streamclient.UdpStreamClient +import com.pedro.udp.UdpClient +import java.nio.ByteBuffer + +/** + * Created by pedro on 6/3/24. + * + * If you use VideoManager.Source.SCREEN/AudioManager.Source.INTERNAL. Call + * changeVideoSourceScreen/changeAudioSourceInternal is necessary to start it. + */ + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +class UdpStream( + context: Context, connectChecker: ConnectChecker, videoSource: VideoSource, + audioSource: AudioSource +): StreamBase(context, videoSource, audioSource) { + + private val streamClientListener = object: StreamClientListener { + override fun onRequestKeyframe() { + requestKeyframe() + } + } + private val udpClient = UdpClient(connectChecker) + private val streamClient = UdpStreamClient(udpClient, streamClientListener) + + override fun getStreamClient(): UdpStreamClient = streamClient + + constructor(context: Context, connectChecker: ConnectChecker): + this(context, connectChecker, Camera2Source(context), MicrophoneSource()) + + override fun setVideoCodecImp(codec: VideoCodec) { + udpClient.setVideoCodec(codec) + } + + override fun setAudioCodecImp(codec: AudioCodec) { + udpClient.setAudioCodec(codec) + } + + override fun audioInfo(sampleRate: Int, isStereo: Boolean) { + udpClient.setAudioInfo(sampleRate, isStereo) + } + + override fun rtpStartStream(endPoint: String) { + udpClient.connect(endPoint) + } + + override fun rtpStopStream() { + udpClient.disconnect() + } + + override fun onSpsPpsVpsRtp(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) { + udpClient.setVideoInfo(sps, pps, vps) + } + + override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { + udpClient.sendVideo(h264Buffer, info) + } + + override fun getAacDataRtp(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) { + udpClient.sendAudio(aacBuffer, info) + } +} \ No newline at end of file diff --git a/library/src/main/java/com/pedro/library/util/streamclient/UdpStreamClient.kt b/library/src/main/java/com/pedro/library/util/streamclient/UdpStreamClient.kt index 68db2010d..62f6042f6 100644 --- a/library/src/main/java/com/pedro/library/util/streamclient/UdpStreamClient.kt +++ b/library/src/main/java/com/pedro/library/util/streamclient/UdpStreamClient.kt @@ -19,7 +19,7 @@ package com.pedro.library.util.streamclient import com.pedro.udp.UdpClient /** - * Created by pedro on 12/10/23. + * Created by pedro on 6/3/24. */ class UdpStreamClient( private val udpClient: UdpClient,