diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 73772f094..7ee12644a 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -50,6 +50,8 @@ afterEvaluate { } dependencies { + implementation(libs.ktor.network) + implementation(libs.ktor.network.tls) implementation(libs.androidx.annotation) implementation(libs.kotlinx.coroutines.android) testImplementation(libs.kotlinx.coroutines.test) diff --git a/common/src/main/java/com/pedro/common/ConnectionFailed.kt b/common/src/main/java/com/pedro/common/ConnectionFailed.kt new file mode 100644 index 000000000..cc4b3e9c4 --- /dev/null +++ b/common/src/main/java/com/pedro/common/ConnectionFailed.kt @@ -0,0 +1,51 @@ +/* + * + * * Copyright (C) 2024 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.common + +/** + * Created by pedro on 23/9/24. + */ +enum class ConnectionFailed { + ENDPOINT_MALFORMED, TIMEOUT, REFUSED, CLOSED_BY_SERVER, NO_INTERNET, UNKNOWN; + + companion object { + fun parse(reason: String): ConnectionFailed { + return if ( + reason.contains("network is unreachable", ignoreCase = true) || + reason.contains("software caused connection abort", ignoreCase = true) || + reason.contains("no route to host", ignoreCase = true) + ) { + NO_INTERNET + } else if (reason.contains("broken pipe", ignoreCase = true)) { + CLOSED_BY_SERVER + } else if (reason.contains("connection refused", ignoreCase = true)) { + REFUSED + } else if (reason.contains("endpoint malformed", ignoreCase = true)) { + ENDPOINT_MALFORMED + } else if ( + reason.contains("timeout", ignoreCase = true) || + reason.contains("timed out", ignoreCase = true) + ) { + TIMEOUT + } else { + UNKNOWN + } + } + } +} \ No newline at end of file diff --git a/common/src/main/java/com/pedro/common/Extensions.kt b/common/src/main/java/com/pedro/common/Extensions.kt index 7ff3b12a1..0d4dfdf5a 100644 --- a/common/src/main/java/com/pedro/common/Extensions.kt +++ b/common/src/main/java/com/pedro/common/Extensions.kt @@ -135,4 +135,8 @@ fun String.getIndexes(char: Char): Array { val indexes = mutableListOf() forEachIndexed { index, c -> if (c == char) indexes.add(index) } return indexes.toTypedArray() +} + +fun Throwable.validMessage(): String { + return (message ?: "").ifEmpty { javaClass.simpleName } } \ No newline at end of file diff --git a/common/src/main/java/com/pedro/common/socket/StreamSocket.kt b/common/src/main/java/com/pedro/common/socket/StreamSocket.kt new file mode 100644 index 000000000..be5dfd7e7 --- /dev/null +++ b/common/src/main/java/com/pedro/common/socket/StreamSocket.kt @@ -0,0 +1,63 @@ +/* + * + * * Copyright (C) 2024 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.common.socket + +import io.ktor.network.selector.SelectorManager +import io.ktor.network.sockets.ReadWriteSocket +import io.ktor.network.sockets.isClosed +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.net.InetAddress +import java.net.InetSocketAddress + +/** + * Created by pedro on 22/9/24. + */ +abstract class StreamSocket( + private val host: String, + private val port: Int +) { + + private var selectorManager = SelectorManager(Dispatchers.IO) + protected var socket: ReadWriteSocket? = null + private var address: InetAddress? = null + + abstract suspend fun buildSocketConfigAndConnect(selectorManager: SelectorManager): ReadWriteSocket + abstract suspend fun closeResources() + + suspend fun connect() { + selectorManager = SelectorManager(Dispatchers.IO) + val socket = buildSocketConfigAndConnect(selectorManager) + address = InetSocketAddress(host, port).address + this.socket = socket + } + + suspend fun close() = withContext(Dispatchers.IO) { + try { + address = null + closeResources() + socket?.close() + selectorManager.close() + } catch (ignored: Exception) {} + } + + fun isConnected(): Boolean = socket?.isClosed != true + + fun isReachable(): Boolean = address?.isReachable(5000) ?: false +} \ No newline at end of file diff --git a/common/src/main/java/com/pedro/common/socket/TcpStreamSocket.kt b/common/src/main/java/com/pedro/common/socket/TcpStreamSocket.kt new file mode 100644 index 000000000..153498372 --- /dev/null +++ b/common/src/main/java/com/pedro/common/socket/TcpStreamSocket.kt @@ -0,0 +1,148 @@ +/* + * + * * Copyright (C) 2024 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.common.socket + +import io.ktor.network.selector.SelectorManager +import io.ktor.network.sockets.InetSocketAddress +import io.ktor.network.sockets.ReadWriteSocket +import io.ktor.network.sockets.aSocket +import io.ktor.network.sockets.openReadChannel +import io.ktor.network.sockets.openWriteChannel +import io.ktor.network.tls.tls +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.ByteWriteChannel +import io.ktor.utils.io.readFully +import io.ktor.utils.io.readUTF8Line +import io.ktor.utils.io.writeByte +import io.ktor.utils.io.writeFully +import io.ktor.utils.io.writeStringUtf8 +import java.net.ConnectException +import java.security.SecureRandom +import javax.net.ssl.TrustManager +import kotlin.coroutines.coroutineContext + +/** + * Created by pedro on 22/9/24. + */ +class TcpStreamSocket( + private val host: String, + private val port: Int, + private val secured: Boolean = false, + private val certificate: TrustManager? = null +): StreamSocket(host, port) { + + private val timeout = 5000L + private var input: ByteReadChannel? = null + private var output: ByteWriteChannel? = null + + override suspend fun buildSocketConfigAndConnect(selectorManager: SelectorManager): ReadWriteSocket { + val builder = aSocket(selectorManager).tcp() + val socket = if (secured) { + builder.connect(remoteAddress = InetSocketAddress(host, port)) { socketTimeout = timeout }.tls( + coroutineContext = coroutineContext + ) { + trustManager = certificate + random = SecureRandom() + } + } else { + builder.connect(host, port) { socketTimeout = timeout } + } + input = socket.openReadChannel() + output = socket.openWriteChannel(autoFlush = false) + return socket + } + + override suspend fun closeResources() { + input = null + output = null + } + + suspend fun flush() { + output?.flush() + } + + suspend fun write(b: Int) { + output?.writeByte(b) + } + + suspend fun write(b: ByteArray) { + output?.writeFully(b) + } + + suspend fun write(b: ByteArray, offset: Int, size: Int) { + output?.writeFully(b, offset, size) + } + + suspend fun writeUInt16(b: Int) { + output?.writeByte(b ushr 8) + output?.writeByte(b) + } + + suspend fun writeUInt24(b: Int) { + output?.writeByte(b ushr 16) + output?.writeByte(b ushr 8) + output?.writeByte(b) + } + + suspend fun writeUInt32(b: Int) { + output?.writeByte(b ushr 24) + output?.writeByte(b ushr 16) + output?.writeByte(b ushr 8) + output?.writeByte(b) + } + + suspend fun writeUInt32LittleEndian(b: Int) { + writeUInt32(Integer.reverseBytes(b)) + } + + suspend fun read(): Int { + val input = input ?: throw ConnectException("Read with socket closed, broken pipe") + return input.readByte().toInt() + } + + suspend fun readUInt16(): Int { + return read() and 0xff shl 8 or (read() and 0xff) + } + + suspend fun readUInt24(): Int { + return read() and 0xff shl 16 or (read() and 0xff shl 8) or (read() and 0xff) + } + + suspend fun readUInt32(): Int { + return read() and 0xff shl 24 or (read() and 0xff shl 16) or (read() and 0xff shl 8) or (read() and 0xff) + } + + suspend fun readUInt32LittleEndian(): Int { + return Integer.reverseBytes(readUInt32()) + } + + suspend fun readUntil(b: ByteArray) { + val input = input ?: throw ConnectException("Read with socket closed, broken pipe") + return input.readFully(b) + } + + suspend fun readLine(): String? { + val input = input ?: throw ConnectException("Read with socket closed, broken pipe") + return input.readUTF8Line() + } + + suspend fun write(string: String) { + output?.writeStringUtf8(string) + } +} \ No newline at end of file diff --git a/common/src/main/java/com/pedro/common/socket/UdpStreamSocket.kt b/common/src/main/java/com/pedro/common/socket/UdpStreamSocket.kt new file mode 100644 index 000000000..aaf16b725 --- /dev/null +++ b/common/src/main/java/com/pedro/common/socket/UdpStreamSocket.kt @@ -0,0 +1,70 @@ +/* + * + * * Copyright (C) 2024 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.common.socket + +import io.ktor.network.selector.SelectorManager +import io.ktor.network.sockets.ConnectedDatagramSocket +import io.ktor.network.sockets.Datagram +import io.ktor.network.sockets.InetSocketAddress +import io.ktor.network.sockets.ReadWriteSocket +import io.ktor.network.sockets.aSocket +import io.ktor.utils.io.core.ByteReadPacket +import io.ktor.utils.io.core.readBytes + +/** + * Created by pedro on 22/9/24. + */ +class UdpStreamSocket( + host: String, + port: Int, + private val sourcePort: Int? = null, + private val receiveSize: Int? = null, + private val broadcastMode: Boolean = false +): StreamSocket(host, port) { + + private val address = InetSocketAddress(host, port) + private val udpSocket by lazy { + socket as ConnectedDatagramSocket + } + + override suspend fun buildSocketConfigAndConnect(selectorManager: SelectorManager): ReadWriteSocket { + val builder = aSocket(selectorManager).udp() + val localAddress = if (sourcePort == null) null else InetSocketAddress("0.0.0.0", sourcePort) + return builder.connect( + remoteAddress = address, + localAddress = localAddress + ) { + broadcast = broadcastMode + receiveBufferSize = receiveSize ?: 0 + } + } + + override suspend fun closeResources() { } + + suspend fun readPacket(): ByteArray { + val packet = udpSocket.receive().packet + val length = packet.remaining.toInt() + return packet.readBytes().sliceArray(0 until length) + } + + suspend fun writePacket(bytes: ByteArray) { + val datagram = Datagram(ByteReadPacket(bytes), address) + udpSocket.send(datagram) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c064bc9c3..575c2b40f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ annotation = "1.8.2" coroutines = "1.9.0" junit = "4.13.2" mockito = "5.4.0" +ktor = "2.3.12" uvcandroid = "1.0.7" [libraries] @@ -31,6 +32,8 @@ androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "mult junit = { module = "junit:junit", version.ref = "junit" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor" } +ktor-network-tls = { module = "io.ktor:ktor-network-tls", version.ref = "ktor" } mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito" } uvcandroid = { module = "com.herohan:UVCAndroid", version.ref = "uvcandroid" } 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 02ba6e339..c6a3e6200 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 @@ -45,7 +45,7 @@ class GenericStreamClient( /** * Add certificates for TLS connection */ - fun addCertificates(certificates: Array?) { + fun addCertificates(certificates: TrustManager?) { rtmpClient.addCertificates(certificates) rtspClient.addCertificates(certificates) } diff --git a/library/src/main/java/com/pedro/library/util/streamclient/RtmpStreamClient.kt b/library/src/main/java/com/pedro/library/util/streamclient/RtmpStreamClient.kt index 6640cc17a..166ff8c64 100644 --- a/library/src/main/java/com/pedro/library/util/streamclient/RtmpStreamClient.kt +++ b/library/src/main/java/com/pedro/library/util/streamclient/RtmpStreamClient.kt @@ -40,7 +40,7 @@ class RtmpStreamClient( /** * Add certificates for TLS connection */ - fun addCertificates(certificates: Array?) { + fun addCertificates(certificates: TrustManager?) { rtmpClient.addCertificates(certificates) } diff --git a/library/src/main/java/com/pedro/library/util/streamclient/RtspStreamClient.kt b/library/src/main/java/com/pedro/library/util/streamclient/RtspStreamClient.kt index 168e25a2e..1dee2172e 100644 --- a/library/src/main/java/com/pedro/library/util/streamclient/RtspStreamClient.kt +++ b/library/src/main/java/com/pedro/library/util/streamclient/RtspStreamClient.kt @@ -31,7 +31,7 @@ class RtspStreamClient( /** * Add certificates for TLS connection */ - fun addCertificates(certificates: Array?) { + fun addCertificates(certificates: TrustManager?) { rtspClient.addCertificates(certificates) } diff --git a/rtmp/src/main/java/com/pedro/rtmp/rtmp/CommandsManager.kt b/rtmp/src/main/java/com/pedro/rtmp/rtmp/CommandsManager.kt index 02c6afdca..8933d9538 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/rtmp/CommandsManager.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/rtmp/CommandsManager.kt @@ -93,13 +93,12 @@ abstract class CommandsManager { @Throws(IOException::class) suspend fun sendChunkSize(socket: RtmpSocket) { writeSync.withLock { - val output = socket.getOutStream() if (RtmpConfig.writeChunkSize != RtmpConfig.DEFAULT_CHUNK_SIZE) { val chunkSize = SetChunkSize(RtmpConfig.writeChunkSize) chunkSize.header.timeStamp = getCurrentTimestamp() chunkSize.header.messageStreamId = streamId - chunkSize.writeHeader(output) - chunkSize.writeBody(output) + chunkSize.writeHeader(socket) + chunkSize.writeBody(socket) socket.flush() Log.i(TAG, "send $chunkSize") } else { @@ -111,8 +110,7 @@ abstract class CommandsManager { @Throws(IOException::class) suspend fun sendConnect(auth: String, socket: RtmpSocket) { writeSync.withLock { - val output = socket.getOutStream() - sendConnect(auth, output) + sendConnectImp(auth, socket) socket.flush() } } @@ -120,16 +118,14 @@ abstract class CommandsManager { @Throws(IOException::class) suspend fun createStream(socket: RtmpSocket) { writeSync.withLock { - val output = socket.getOutStream() - createStream(output) + createStreamImp(socket) socket.flush() } } @Throws(IOException::class) - fun readMessageResponse(socket: RtmpSocket): RtmpMessage { - val input = socket.getInputStream() - val message = RtmpMessage.getRtmpMessage(input, readChunkSize, sessionHistory) + suspend fun readMessageResponse(socket: RtmpSocket): RtmpMessage { + val message = RtmpMessage.getRtmpMessage(socket, readChunkSize, sessionHistory) sessionHistory.setReadHeader(message.header) Log.i(TAG, "read $message") bytesRead += message.header.getPacketLength() @@ -139,8 +135,7 @@ abstract class CommandsManager { @Throws(IOException::class) suspend fun sendMetadata(socket: RtmpSocket) { writeSync.withLock { - val output = socket.getOutStream() - sendMetadata(output) + sendMetadataImp(socket) socket.flush() } } @@ -148,8 +143,7 @@ abstract class CommandsManager { @Throws(IOException::class) suspend fun sendPublish(socket: RtmpSocket) { writeSync.withLock { - val output = socket.getOutStream() - sendPublish(output) + sendPublishImp(socket) socket.flush() } } @@ -157,20 +151,18 @@ abstract class CommandsManager { @Throws(IOException::class) suspend fun sendWindowAcknowledgementSize(socket: RtmpSocket) { writeSync.withLock { - val output = socket.getOutStream() val windowAcknowledgementSize = WindowAcknowledgementSize(RtmpConfig.acknowledgementWindowSize, getCurrentTimestamp()) - windowAcknowledgementSize.writeHeader(output) - windowAcknowledgementSize.writeBody(output) + windowAcknowledgementSize.writeHeader(socket) + windowAcknowledgementSize.writeBody(socket) socket.flush() } } suspend fun sendPong(event: Event, socket: RtmpSocket) { writeSync.withLock { - val output = socket.getOutStream() val pong = UserControl(Type.PONG_REPLY, event) - pong.writeHeader(output) - pong.writeBody(output) + pong.writeHeader(socket) + pong.writeBody(socket) socket.flush() Log.i(TAG, "send pong") } @@ -179,8 +171,7 @@ abstract class CommandsManager { @Throws(IOException::class) suspend fun sendClose(socket: RtmpSocket) { writeSync.withLock { - val output = socket.getOutStream() - sendClose(output) + sendCloseImp(socket) socket.flush() } } @@ -190,11 +181,10 @@ abstract class CommandsManager { if (bytesRead >= RtmpConfig.acknowledgementWindowSize) { acknowledgementSequence += bytesRead bytesRead -= RtmpConfig.acknowledgementWindowSize - val output = socket.getOutStream() val acknowledgement = Acknowledgement(acknowledgementSequence) - acknowledgement.writeHeader(output) - acknowledgement.writeBody(output) - output.flush() + acknowledgement.writeHeader(socket) + acknowledgement.writeBody(socket) + socket.flush() Log.i(TAG, "send $acknowledgement") } } @@ -203,13 +193,12 @@ abstract class CommandsManager { @Throws(IOException::class) suspend fun sendVideoPacket(flvPacket: FlvPacket, socket: RtmpSocket): Int { writeSync.withLock { - val output = socket.getOutStream() if (incrementalTs) { flvPacket.timeStamp = ((TimeUtils.getCurrentTimeNano() / 1000 - startTs) / 1000) } val video = Video(flvPacket, streamId) - video.writeHeader(output) - video.writeBody(output) + video.writeHeader(socket) + video.writeBody(socket) socket.flush(true) return video.header.getPacketLength() //get packet size with header included to calculate bps } @@ -218,23 +207,22 @@ abstract class CommandsManager { @Throws(IOException::class) suspend fun sendAudioPacket(flvPacket: FlvPacket, socket: RtmpSocket): Int { writeSync.withLock { - val output = socket.getOutStream() if (incrementalTs) { flvPacket.timeStamp = ((TimeUtils.getCurrentTimeNano() / 1000 - startTs) / 1000) } val audio = Audio(flvPacket, streamId) - audio.writeHeader(output) - audio.writeBody(output) + audio.writeHeader(socket) + audio.writeBody(socket) socket.flush(true) return audio.header.getPacketLength() //get packet size with header included to calculate bps } } - abstract fun sendConnect(auth: String, output: OutputStream) - abstract fun createStream(output: OutputStream) - abstract fun sendMetadata(output: OutputStream) - abstract fun sendPublish(output: OutputStream) - abstract fun sendClose(output: OutputStream) + abstract suspend fun sendConnectImp(auth: String, socket: RtmpSocket) + abstract suspend fun createStreamImp(socket: RtmpSocket) + abstract suspend fun sendMetadataImp(socket: RtmpSocket) + abstract suspend fun sendPublishImp(socket: RtmpSocket) + abstract suspend fun sendCloseImp(socket: RtmpSocket) fun reset() { startTs = 0 diff --git a/rtmp/src/main/java/com/pedro/rtmp/rtmp/CommandsManagerAmf0.kt b/rtmp/src/main/java/com/pedro/rtmp/rtmp/CommandsManagerAmf0.kt index 5a4d81f23..49304e25f 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/rtmp/CommandsManagerAmf0.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/rtmp/CommandsManagerAmf0.kt @@ -32,10 +32,10 @@ import com.pedro.rtmp.rtmp.chunk.ChunkType import com.pedro.rtmp.rtmp.message.BasicHeader import com.pedro.rtmp.rtmp.message.command.CommandAmf0 import com.pedro.rtmp.rtmp.message.data.DataAmf0 -import java.io.OutputStream +import com.pedro.rtmp.utils.socket.RtmpSocket class CommandsManagerAmf0: CommandsManager() { - override fun sendConnect(auth: String, output: OutputStream) { + override suspend fun sendConnectImp(auth: String, socket: RtmpSocket) { val connect = CommandAmf0("connect", ++commandId, getCurrentTimestamp(), streamId, BasicHeader(ChunkType.TYPE_0, ChunkStreamId.OVER_CONNECTION.mark)) val connectInfo = AmfObject() @@ -58,20 +58,20 @@ class CommandsManagerAmf0: CommandsManager() { connectInfo.setProperty("objectEncoding", 0.0) connect.addData(connectInfo) - connect.writeHeader(output) - connect.writeBody(output) + connect.writeHeader(socket) + connect.writeBody(socket) sessionHistory.setPacket(commandId, "connect") Log.i(TAG, "send $connect") } - override fun createStream(output: OutputStream) { + override suspend fun createStreamImp(socket: RtmpSocket) { val releaseStream = CommandAmf0("releaseStream", ++commandId, getCurrentTimestamp(), streamId, BasicHeader(ChunkType.TYPE_0, ChunkStreamId.OVER_STREAM.mark)) releaseStream.addData(AmfNull()) releaseStream.addData(AmfString(streamName)) - releaseStream.writeHeader(output) - releaseStream.writeBody(output) + releaseStream.writeHeader(socket) + releaseStream.writeBody(socket) sessionHistory.setPacket(commandId, "releaseStream") Log.i(TAG, "send $releaseStream") @@ -80,8 +80,8 @@ class CommandsManagerAmf0: CommandsManager() { fcPublish.addData(AmfNull()) fcPublish.addData(AmfString(streamName)) - fcPublish.writeHeader(output) - fcPublish.writeBody(output) + fcPublish.writeHeader(socket) + fcPublish.writeBody(socket) sessionHistory.setPacket(commandId, "FCPublish") Log.i(TAG, "send $fcPublish") @@ -89,13 +89,13 @@ class CommandsManagerAmf0: CommandsManager() { BasicHeader(ChunkType.TYPE_0, ChunkStreamId.OVER_CONNECTION.mark)) createStream.addData(AmfNull()) - createStream.writeHeader(output) - createStream.writeBody(output) + createStream.writeHeader(socket) + createStream.writeBody(socket) sessionHistory.setPacket(commandId, "createStream") Log.i(TAG, "send $createStream") } - override fun sendMetadata(output: OutputStream) { + override suspend fun sendMetadataImp(socket: RtmpSocket) { val name = "@setDataFrame" val metadata = DataAmf0(name, getCurrentTimestamp(), streamId) metadata.addData(AmfString("onMetaData")) @@ -129,12 +129,12 @@ class CommandsManagerAmf0: CommandsManager() { amfEcmaArray.setProperty("filesize", 0.0) metadata.addData(amfEcmaArray) - metadata.writeHeader(output) - metadata.writeBody(output) + metadata.writeHeader(socket) + metadata.writeBody(socket) Log.i(TAG, "send $metadata") } - override fun sendPublish(output: OutputStream) { + override suspend fun sendPublishImp(socket: RtmpSocket) { val name = "publish" val publish = CommandAmf0(name, ++commandId, getCurrentTimestamp(), streamId, BasicHeader(ChunkType.TYPE_0, ChunkStreamId.OVER_STREAM.mark)) @@ -142,19 +142,19 @@ class CommandsManagerAmf0: CommandsManager() { publish.addData(AmfString(streamName)) publish.addData(AmfString("live")) - publish.writeHeader(output) - publish.writeBody(output) + publish.writeHeader(socket) + publish.writeBody(socket) sessionHistory.setPacket(commandId, name) Log.i(TAG, "send $publish") } - override fun sendClose(output: OutputStream) { + override suspend fun sendCloseImp(socket: RtmpSocket) { val name = "closeStream" val closeStream = CommandAmf0(name, ++commandId, getCurrentTimestamp(), streamId, BasicHeader(ChunkType.TYPE_0, ChunkStreamId.OVER_STREAM.mark)) closeStream.addData(AmfNull()) - closeStream.writeHeader(output) - closeStream.writeBody(output) + closeStream.writeHeader(socket) + closeStream.writeBody(socket) sessionHistory.setPacket(commandId, name) Log.i(TAG, "send $closeStream") } diff --git a/rtmp/src/main/java/com/pedro/rtmp/rtmp/CommandsManagerAmf3.kt b/rtmp/src/main/java/com/pedro/rtmp/rtmp/CommandsManagerAmf3.kt index 28dd2eeb4..060c21054 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/rtmp/CommandsManagerAmf3.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/rtmp/CommandsManagerAmf3.kt @@ -31,10 +31,10 @@ import com.pedro.rtmp.rtmp.chunk.ChunkType import com.pedro.rtmp.rtmp.message.BasicHeader import com.pedro.rtmp.rtmp.message.command.CommandAmf3 import com.pedro.rtmp.rtmp.message.data.DataAmf3 -import java.io.OutputStream +import com.pedro.rtmp.utils.socket.RtmpSocket class CommandsManagerAmf3: CommandsManager() { - override fun sendConnect(auth: String, output: OutputStream) { + override suspend fun sendConnectImp(auth: String, socket: RtmpSocket) { val connect = CommandAmf3("connect", ++commandId, getCurrentTimestamp(), streamId, BasicHeader(ChunkType.TYPE_0, ChunkStreamId.OVER_CONNECTION.mark)) val connectInfo = Amf3Object() @@ -57,20 +57,20 @@ class CommandsManagerAmf3: CommandsManager() { connectInfo.setProperty("objectEncoding", 3.0) connect.addData(connectInfo) - connect.writeHeader(output) - connect.writeBody(output) + connect.writeHeader(socket) + connect.writeBody(socket) sessionHistory.setPacket(commandId, "connect") Log.i(TAG, "send $connect") } - override fun createStream(output: OutputStream) { + override suspend fun createStreamImp(socket: RtmpSocket) { val releaseStream = CommandAmf3("releaseStream", ++commandId, getCurrentTimestamp(), streamId, BasicHeader(ChunkType.TYPE_0, ChunkStreamId.OVER_STREAM.mark)) releaseStream.addData(Amf3Null()) releaseStream.addData(Amf3String(streamName)) - releaseStream.writeHeader(output) - releaseStream.writeBody(output) + releaseStream.writeHeader(socket) + releaseStream.writeBody(socket) sessionHistory.setPacket(commandId, "releaseStream") Log.i(TAG, "send $releaseStream") @@ -79,8 +79,8 @@ class CommandsManagerAmf3: CommandsManager() { fcPublish.addData(Amf3Null()) fcPublish.addData(Amf3String(streamName)) - fcPublish.writeHeader(output) - fcPublish.writeBody(output) + fcPublish.writeHeader(socket) + fcPublish.writeBody(socket) sessionHistory.setPacket(commandId, "FCPublish") Log.i(TAG, "send $fcPublish") @@ -88,13 +88,13 @@ class CommandsManagerAmf3: CommandsManager() { BasicHeader(ChunkType.TYPE_0, ChunkStreamId.OVER_CONNECTION.mark)) createStream.addData(Amf3Null()) - createStream.writeHeader(output) - createStream.writeBody(output) + createStream.writeHeader(socket) + createStream.writeBody(socket) sessionHistory.setPacket(commandId, "createStream") Log.i(TAG, "send $createStream") } - override fun sendMetadata(output: OutputStream) { + override suspend fun sendMetadataImp(socket: RtmpSocket) { val name = "@setDataFrame" val metadata = DataAmf3(name, getCurrentTimestamp(), streamId) metadata.addData(Amf3String("onMetaData")) @@ -120,12 +120,12 @@ class CommandsManagerAmf3: CommandsManager() { amfEcmaArray.setProperty("filesize", 0.0) metadata.addData(amfEcmaArray) - metadata.writeHeader(output) - metadata.writeBody(output) + metadata.writeHeader(socket) + metadata.writeBody(socket) Log.i(TAG, "send $metadata") } - override fun sendPublish(output: OutputStream) { + override suspend fun sendPublishImp(socket: RtmpSocket) { val name = "publish" val publish = CommandAmf3(name, ++commandId, getCurrentTimestamp(), streamId, BasicHeader(ChunkType.TYPE_0, ChunkStreamId.OVER_STREAM.mark)) @@ -133,19 +133,19 @@ class CommandsManagerAmf3: CommandsManager() { publish.addData(Amf3String(streamName)) publish.addData(Amf3String("live")) - publish.writeHeader(output) - publish.writeBody(output) + publish.writeHeader(socket) + publish.writeBody(socket) sessionHistory.setPacket(commandId, name) Log.i(TAG, "send $publish") } - override fun sendClose(output: OutputStream) { + override suspend fun sendCloseImp(socket: RtmpSocket) { val name = "closeStream" val closeStream = CommandAmf3(name, ++commandId, getCurrentTimestamp(), streamId, BasicHeader(ChunkType.TYPE_0, ChunkStreamId.OVER_STREAM.mark)) closeStream.addData(Amf3Null()) - closeStream.writeHeader(output) - closeStream.writeBody(output) + closeStream.writeHeader(socket) + closeStream.writeBody(socket) sessionHistory.setPacket(commandId, name) Log.i(TAG, "send $closeStream") } diff --git a/rtmp/src/main/java/com/pedro/rtmp/rtmp/Handshake.kt b/rtmp/src/main/java/com/pedro/rtmp/rtmp/Handshake.kt index 2426c8791..f2e7b4509 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/rtmp/Handshake.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/rtmp/Handshake.kt @@ -75,31 +75,27 @@ class Handshake { private var timestampC1 = 0 @Throws(IOException::class) - fun sendHandshake(socket: RtmpSocket): Boolean { - var output = socket.getOutStream() - writeC0(output) - val c1 = writeC1(output) + suspend fun sendHandshake(socket: RtmpSocket): Boolean { + writeC0(socket) + val c1 = writeC1(socket) socket.flush() - var input = socket.getInputStream() - readS0(input) - val s1 = readS1(input) - output = socket.getOutStream() - writeC2(output, s1) + readS0(socket) + val s1 = readS1(socket) + writeC2(socket, s1) socket.flush() - input = socket.getInputStream() - readS2(input, c1) + readS2(socket, c1) return true } @Throws(IOException::class) - private fun writeC0(output: OutputStream) { + private suspend fun writeC0(socket: RtmpSocket) { Log.i(TAG, "writing C0") - output.write(protocolVersion) + socket.write(protocolVersion) Log.i(TAG, "C0 write successful") } @Throws(IOException::class) - private fun writeC1(output: OutputStream): ByteArray { + private suspend fun writeC1(socket: RtmpSocket): ByteArray { Log.i(TAG, "writing C1") val c1 = ByteArray(handshakeSize) @@ -125,22 +121,22 @@ class Handshake { randomData[i] = uInt8 } System.arraycopy(randomData, 0, c1, 8, randomData.size) - output.write(c1) + socket.write(c1) Log.i(TAG, "C1 write successful") return c1 } @Throws(IOException::class) - private fun writeC2(output: OutputStream, s1: ByteArray) { + private suspend fun writeC2(socket: RtmpSocket, s1: ByteArray) { Log.i(TAG, "writing C2") - output.write(s1) + socket.write(s1) Log.i(TAG, "C2 write successful") } @Throws(IOException::class) - private fun readS0(input: InputStream): ByteArray { + private suspend fun readS0(socket: RtmpSocket): ByteArray { Log.i(TAG, "reading S0") - val response = input.read() + val response = socket.read() if (response == protocolVersion || response == 72) { Log.i(TAG, "read S0 successful") return byteArrayOf(response.toByte()) @@ -150,19 +146,19 @@ class Handshake { } @Throws(IOException::class) - private fun readS1(input: InputStream): ByteArray { + private suspend fun readS1(socket: RtmpSocket): ByteArray { Log.i(TAG, "reading S1") val s1 = ByteArray(handshakeSize) - input.readUntil(s1) + socket.readUntil(s1) Log.i(TAG, "read S1 successful") return s1 } @Throws(IOException::class) - private fun readS2(input: InputStream, c1: ByteArray): ByteArray { + private suspend fun readS2(socket: RtmpSocket, c1: ByteArray): ByteArray { Log.i(TAG, "reading S2") val s2 = ByteArray(handshakeSize) - input.readUntil(s2) + socket.readUntil(s2) //S2 should be equals to C1 but we can skip this if (!s2.contentEquals(c1)) { Log.e(TAG, "S2 content is different that C1") diff --git a/rtmp/src/main/java/com/pedro/rtmp/rtmp/RtmpClient.kt b/rtmp/src/main/java/com/pedro/rtmp/rtmp/RtmpClient.kt index a0d76495a..f72f17901 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/rtmp/RtmpClient.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/rtmp/RtmpClient.kt @@ -20,10 +20,12 @@ import android.media.MediaCodec import android.util.Log import com.pedro.common.AudioCodec import com.pedro.common.ConnectChecker +import com.pedro.common.ConnectionFailed import com.pedro.common.TimeUtils import com.pedro.common.UrlParser import com.pedro.common.VideoCodec import com.pedro.common.onMainThread +import com.pedro.common.validMessage import com.pedro.rtmp.amf.AmfVersion import com.pedro.rtmp.rtmp.message.* import com.pedro.rtmp.rtmp.message.command.Command @@ -71,7 +73,7 @@ class RtmpClient(private val connectChecker: ConnectChecker) { private var url: String? = null private var tlsEnabled = false - private var certificates: Array? = null + private var certificates: TrustManager? = null private var tunneled = false private var doingRetry = false @@ -95,7 +97,7 @@ class RtmpClient(private val connectChecker: ConnectChecker) { /** * Add certificates for TLS connection */ - fun addCertificates(certificates: Array?) { + fun addCertificates(certificates: TrustManager?) { this.certificates = certificates } @@ -269,7 +271,7 @@ class RtmpClient(private val connectChecker: ConnectChecker) { if (error != null) { Log.e(TAG, "connection error", error) onMainThread { - connectChecker.onConnectionFailed("Error configure stream, ${error.message}") + connectChecker.onConnectionFailed("Error configure stream, ${error.validMessage()}") } return@launch } @@ -281,6 +283,7 @@ class RtmpClient(private val connectChecker: ConnectChecker) { while (scope.isActive && isStreaming) { val error = runCatching { if (isAlive()) { + delay(2000) //ignore packet after connect if tunneled to avoid spam idle if (!tunneled) handleMessages() } else { @@ -290,7 +293,7 @@ class RtmpClient(private val connectChecker: ConnectChecker) { scope.cancel() } }.exceptionOrNull() - if (error != null && error !is SocketTimeoutException) { + if (error != null && ConnectionFailed.parse(error.validMessage()) != ConnectionFailed.TIMEOUT) { scope.cancel() } } @@ -308,7 +311,7 @@ class RtmpClient(private val connectChecker: ConnectChecker) { } @Throws(IOException::class) - private fun establishConnection(): Boolean { + private suspend fun establishConnection(): Boolean { val socket = if (tunneled) { TcpTunneledSocket(commandsManager.host, commandsManager.port, tlsEnabled) } else { @@ -491,7 +494,7 @@ class RtmpClient(private val connectChecker: ConnectChecker) { } } - fun closeConnection() { + private suspend fun closeConnection() { socket?.close() commandsManager.reset() } diff --git a/rtmp/src/main/java/com/pedro/rtmp/rtmp/RtmpSender.kt b/rtmp/src/main/java/com/pedro/rtmp/rtmp/RtmpSender.kt index 7de6f83b6..25bc41c86 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/rtmp/RtmpSender.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/rtmp/RtmpSender.kt @@ -24,6 +24,7 @@ import com.pedro.common.ConnectChecker import com.pedro.common.VideoCodec import com.pedro.common.onMainThread import com.pedro.common.trySend +import com.pedro.common.validMessage import com.pedro.rtmp.flv.BasePacket import com.pedro.rtmp.flv.FlvPacket import com.pedro.rtmp.flv.FlvType @@ -183,7 +184,7 @@ class RtmpSender( }.exceptionOrNull() if (error != null) { onMainThread { - connectChecker.onConnectionFailed("Error send packet, " + error.message) + connectChecker.onConnectionFailed("Error send packet, ${error.validMessage()}") } Log.e(TAG, "send error: ", error) return@launch diff --git a/rtmp/src/main/java/com/pedro/rtmp/rtmp/message/BasicHeader.kt b/rtmp/src/main/java/com/pedro/rtmp/rtmp/message/BasicHeader.kt index a4fb8555f..73e0a6f77 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/rtmp/message/BasicHeader.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/rtmp/message/BasicHeader.kt @@ -17,6 +17,7 @@ package com.pedro.rtmp.rtmp.message import com.pedro.rtmp.rtmp.chunk.ChunkType +import com.pedro.rtmp.utils.socket.RtmpSocket import java.io.IOException import java.io.InputStream import kotlin.experimental.and @@ -59,17 +60,17 @@ import kotlin.experimental.and class BasicHeader(val chunkType: ChunkType, val chunkStreamId: Int) { companion object { - fun parseBasicHeader(input: InputStream): BasicHeader { - val byte = input.read().toByte() + suspend fun parseBasicHeader(socket: RtmpSocket): BasicHeader { + val byte = socket.read().toByte() val chunkTypeValue = 0xff and byte.toInt() ushr 6 val chunkType = ChunkType.entries.find { it.mark.toInt() == chunkTypeValue } ?: throw IOException("Unknown chunk type value: $chunkTypeValue") var chunkStreamIdValue = (byte and 0x3F).toInt() if (chunkStreamIdValue > 63) throw IOException("Unknown chunk stream id value: $chunkStreamIdValue") if (chunkStreamIdValue == 0) { //Basic header 2 bytes - chunkStreamIdValue = input.read() - 64 + chunkStreamIdValue = socket.read() - 64 } else if (chunkStreamIdValue == 1) { //Basic header 3 bytes - val a = input.read() - val b = input.read() + val a = socket.read() + val b = socket.read() val value = b and 0xff shl 8 and a chunkStreamIdValue = value - 64 } diff --git a/rtmp/src/main/java/com/pedro/rtmp/rtmp/message/RtmpHeader.kt b/rtmp/src/main/java/com/pedro/rtmp/rtmp/message/RtmpHeader.kt index b934c0fc2..7d0eb86c4 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/rtmp/message/RtmpHeader.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/rtmp/message/RtmpHeader.kt @@ -18,6 +18,7 @@ package com.pedro.rtmp.rtmp.message import com.pedro.rtmp.rtmp.chunk.ChunkType import com.pedro.rtmp.utils.* +import com.pedro.rtmp.utils.socket.RtmpSocket import java.io.IOException import java.io.InputStream import java.io.OutputStream @@ -41,9 +42,9 @@ class RtmpHeader(var basicHeader: BasicHeader) { * Check ChunkType class to know header structure */ @Throws(IOException::class) - fun readHeader(input: InputStream, commandSessionHistory: CommandSessionHistory, + suspend fun readHeader(socket: RtmpSocket, commandSessionHistory: CommandSessionHistory, timestamp: Int = 0): RtmpHeader { - val basicHeader = BasicHeader.parseBasicHeader(input) + val basicHeader = BasicHeader.parseBasicHeader(socket) var timeStamp = timestamp var messageLength = 0 var messageType: MessageType? = null @@ -51,25 +52,25 @@ class RtmpHeader(var basicHeader: BasicHeader) { val lastHeader = commandSessionHistory.getLastReadHeader(basicHeader.chunkStreamId) when (basicHeader.chunkType) { ChunkType.TYPE_0 -> { - timeStamp = input.readUInt24() - messageLength = input.readUInt24() - messageType = RtmpMessage.getMarkType(input.read()) - messageStreamId = input.readUInt32LittleEndian() + timeStamp = socket.readUInt24() + messageLength = socket.readUInt24() + messageType = RtmpMessage.getMarkType(socket.read()) + messageStreamId = socket.readUInt32LittleEndian() //extended timestamp if (timeStamp >= 0xffffff) { - timeStamp = input.readUInt32() + timeStamp = socket.readUInt32() } } ChunkType.TYPE_1 -> { if (lastHeader != null) { messageStreamId = lastHeader.messageStreamId } - timeStamp = input.readUInt24() - messageLength = input.readUInt24() - messageType = RtmpMessage.getMarkType(input.read()) + timeStamp = socket.readUInt24() + messageLength = socket.readUInt24() + messageType = RtmpMessage.getMarkType(socket.read()) //extended timestamp if (timeStamp >= 0xffffff) { - timeStamp = input.readUInt32() + timeStamp = socket.readUInt32() } } ChunkType.TYPE_2 -> { @@ -78,10 +79,10 @@ class RtmpHeader(var basicHeader: BasicHeader) { messageType = lastHeader.messageType messageStreamId = lastHeader.messageStreamId } - timeStamp = input.readUInt24() + timeStamp = socket.readUInt24() //extended timestamp if (timeStamp >= 0xffffff) { - timeStamp = input.readUInt32() + timeStamp = socket.readUInt32() } } ChunkType.TYPE_3 -> { @@ -93,7 +94,7 @@ class RtmpHeader(var basicHeader: BasicHeader) { } //extended timestamp if (timeStamp >= 0xffffff) { - timeStamp = input.readUInt32() + timeStamp = socket.readUInt32() } //No header to read } @@ -108,52 +109,51 @@ class RtmpHeader(var basicHeader: BasicHeader) { } @Throws(IOException::class) - fun writeHeader(output: OutputStream) { - writeHeader(basicHeader, output) + suspend fun writeHeader(socket: RtmpSocket) { + writeHeader(basicHeader, socket) } /** * Check ChunkType class to know header structure */ - @Throws(IOException::class) - fun writeHeader(basicHeader: BasicHeader, output: OutputStream) { + suspend fun writeHeader(basicHeader: BasicHeader, socket: RtmpSocket) { // Write basic header byte - output.write((basicHeader.chunkType.mark.toInt() shl 6) or basicHeader.chunkStreamId) + socket.write((basicHeader.chunkType.mark.toInt() shl 6) or basicHeader.chunkStreamId) when (basicHeader.chunkType) { ChunkType.TYPE_0 -> { - output.writeUInt24(min(timeStamp, 0xffffff)) - output.writeUInt24(messageLength) + socket.writeUInt24(min(timeStamp, 0xffffff)) + socket.writeUInt24(messageLength) messageType?.let { messageType -> - output.write(messageType.mark.toInt()) + socket.write(messageType.mark.toInt()) } - output.writeUInt32LittleEndian(messageStreamId) + socket.writeUInt32LittleEndian(messageStreamId) //extended timestamp if (timeStamp > 0xffffff) { - output.writeUInt32(timeStamp) + socket.writeUInt32(timeStamp) } } ChunkType.TYPE_1 -> { - output.writeUInt24(min(timeStamp, 0xffffff)) - output.writeUInt24(messageLength) + socket.writeUInt24(min(timeStamp, 0xffffff)) + socket.writeUInt24(messageLength) messageType?.let { messageType -> - output.write(messageType.mark.toInt()) + socket.write(messageType.mark.toInt()) } //extended timestamp if (timeStamp > 0xffffff) { - output.writeUInt32(timeStamp) + socket.writeUInt32(timeStamp) } } ChunkType.TYPE_2 -> { - output.writeUInt24(min(timeStamp, 0xffffff)) + socket.writeUInt24(min(timeStamp, 0xffffff)) //extended timestamp if (timeStamp > 0xffffff) { - output.writeUInt32(timeStamp) + socket.writeUInt32(timeStamp) } } ChunkType.TYPE_3 -> { //extended timestamp if (timeStamp > 0xffffff) { - output.writeUInt32(timeStamp) + socket.writeUInt32(timeStamp) } } } diff --git a/rtmp/src/main/java/com/pedro/rtmp/rtmp/message/RtmpMessage.kt b/rtmp/src/main/java/com/pedro/rtmp/rtmp/message/RtmpMessage.kt index 5e73430c4..b5797cf5d 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/rtmp/message/RtmpMessage.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/rtmp/message/RtmpMessage.kt @@ -26,7 +26,7 @@ import com.pedro.rtmp.rtmp.message.shared.SharedObjectAmf0 import com.pedro.rtmp.rtmp.message.shared.SharedObjectAmf3 import com.pedro.rtmp.utils.CommandSessionHistory import com.pedro.rtmp.utils.RtmpConfig -import com.pedro.rtmp.utils.readUntil +import com.pedro.rtmp.utils.socket.RtmpSocket import java.io.* /** @@ -46,9 +46,9 @@ abstract class RtmpMessage(basicHeader: BasicHeader) { private const val TAG = "RtmpMessage" @Throws(IOException::class) - fun getRtmpMessage(input: InputStream, chunkSize: Int, + suspend fun getRtmpMessage(socket: RtmpSocket, chunkSize: Int, commandSessionHistory: CommandSessionHistory): RtmpMessage { - val header = RtmpHeader.readHeader(input, commandSessionHistory) + val header = RtmpHeader.readHeader(socket, commandSessionHistory) val rtmpMessage = when (header.messageType) { MessageType.SET_CHUNK_SIZE -> SetChunkSize() MessageType.ABORT -> Abort() @@ -70,9 +70,11 @@ abstract class RtmpMessage(basicHeader: BasicHeader) { rtmpMessage.updateHeader(header) //we have multiple chunk wait until we have full body on stream and discard chunk header val bodyInput = if (header.messageLength > chunkSize) { - getInputWithoutChunks(input, header, chunkSize, commandSessionHistory) + getInputWithoutChunks(socket, header, chunkSize, commandSessionHistory) } else { - input + val bytes = ByteArray(header.messageLength) + socket.readUntil(bytes) + ByteArrayInputStream(bytes) } rtmpMessage.readBody(bodyInput) return rtmpMessage @@ -82,7 +84,7 @@ abstract class RtmpMessage(basicHeader: BasicHeader) { return MessageType.entries.find { it.mark.toInt() == type } ?: throw IOException("Unknown rtmp message type: $type") } - private fun getInputWithoutChunks(input: InputStream, header: RtmpHeader, chunkSize: Int, + private suspend fun getInputWithoutChunks(socket: RtmpSocket, header: RtmpHeader, chunkSize: Int, commandSessionHistory: CommandSessionHistory): InputStream { val packetStore = ByteArrayOutputStream() var bytesRead = 0 @@ -91,12 +93,12 @@ abstract class RtmpMessage(basicHeader: BasicHeader) { if (header.messageLength - bytesRead < chunkSize) { //last chunk chunk = ByteArray(header.messageLength - bytesRead) - input.readUntil(chunk) + socket.readUntil(chunk) } else { chunk = ByteArray(chunkSize) - input.readUntil(chunk) + socket.readUntil(chunk) //skip chunk header to discard it, set packet ts to indicate if you need read extended ts - RtmpHeader.readHeader(input, commandSessionHistory, header.timeStamp) + RtmpHeader.readHeader(socket, commandSessionHistory, header.timeStamp) } bytesRead += chunk.size packetStore.write(chunk) @@ -113,13 +115,11 @@ abstract class RtmpMessage(basicHeader: BasicHeader) { header.timeStamp = rtmpHeader.timeStamp } - @Throws(IOException::class) - fun writeHeader(output: OutputStream) { - header.writeHeader(output) + suspend fun writeHeader(socket: RtmpSocket) { + header.writeHeader(socket) } - @Throws(IOException::class) - fun writeBody(output: OutputStream) { + suspend fun writeBody(socket: RtmpSocket) { val chunkSize = RtmpConfig.writeChunkSize val bytes = storeBody() var pos = 0 @@ -127,13 +127,13 @@ abstract class RtmpMessage(basicHeader: BasicHeader) { while (length > chunkSize) { // Write packet for chunk - output.write(bytes, pos, chunkSize) + socket.write(bytes, pos, chunkSize) length -= chunkSize pos += chunkSize // Write header for remain chunk - header.writeHeader(BasicHeader(ChunkType.TYPE_3, header.basicHeader.chunkStreamId), output) + header.writeHeader(BasicHeader(ChunkType.TYPE_3, header.basicHeader.chunkStreamId), socket) } - output.write(bytes, pos, length) + socket.write(bytes, pos, length) } abstract fun readBody(input: InputStream) diff --git a/rtmp/src/main/java/com/pedro/rtmp/utils/socket/RtmpSocket.kt b/rtmp/src/main/java/com/pedro/rtmp/utils/socket/RtmpSocket.kt index 968ed44df..e3aa49608 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/utils/socket/RtmpSocket.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/utils/socket/RtmpSocket.kt @@ -16,9 +16,6 @@ package com.pedro.rtmp.utils.socket -import java.io.InputStream -import java.io.OutputStream - /** * Created by pedro on 5/4/22. * Socket implementation that accept: @@ -30,13 +27,22 @@ import java.io.OutputStream */ abstract class RtmpSocket { - protected val timeout = 5000 - - abstract fun getOutStream(): OutputStream - abstract fun getInputStream(): InputStream - abstract fun flush(isPacket: Boolean = false) - abstract fun connect() - abstract fun close() + abstract suspend fun flush(isPacket: Boolean = false) + abstract suspend fun connect() + abstract suspend fun close() abstract fun isConnected(): Boolean abstract fun isReachable(): Boolean + abstract suspend fun write(b: Int) + abstract suspend fun write(b: ByteArray) + abstract suspend fun write(b: ByteArray, offset: Int, size: Int) + abstract suspend fun writeUInt16(b: Int) + abstract suspend fun writeUInt24(b: Int) + abstract suspend fun writeUInt32(b: Int) + abstract suspend fun writeUInt32LittleEndian(b: Int) + abstract suspend fun read(): Int + abstract suspend fun readUInt16(): Int + abstract suspend fun readUInt24(): Int + abstract suspend fun readUInt32(): Int + abstract suspend fun readUInt32LittleEndian(): Int + abstract suspend fun readUntil(b: ByteArray) } \ No newline at end of file diff --git a/rtmp/src/main/java/com/pedro/rtmp/utils/socket/TcpSocket.kt b/rtmp/src/main/java/com/pedro/rtmp/utils/socket/TcpSocket.kt index fc781cf26..8e199efd5 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/utils/socket/TcpSocket.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/utils/socket/TcpSocket.kt @@ -16,70 +16,73 @@ package com.pedro.rtmp.utils.socket -import com.pedro.common.TLSSocketFactory -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.net.InetSocketAddress -import java.net.Socket -import java.net.SocketAddress -import java.security.GeneralSecurityException +import com.pedro.common.socket.TcpStreamSocket import javax.net.ssl.TrustManager /** * Created by pedro on 5/4/22. */ class TcpSocket( - private val host: String, - private val port: Int, - private val secured: Boolean, - private val certificates: Array? + host: String, port: Int, secured: Boolean, certificates: TrustManager? ): RtmpSocket() { - private var socket: Socket = Socket() - private var input = ByteArrayInputStream(byteArrayOf()).buffered() - private var output = ByteArrayOutputStream().buffered() + private val socket = TcpStreamSocket(host, port, secured, certificates) - override fun getOutStream(): OutputStream = output + override suspend fun flush(isPacket: Boolean) { + socket.flush() + } + + override suspend fun connect() { + socket.connect() + } + + override suspend fun close() { + socket.close() + } - override fun getInputStream(): InputStream = input + override fun isConnected(): Boolean = socket.isConnected() - override fun flush(isPacket: Boolean) { - getOutStream().flush() + override fun isReachable(): Boolean = socket.isReachable() + + override suspend fun write(b: Int) { + socket.write(b) } - override fun connect() { - if (secured) { - try { - val socketFactory = TLSSocketFactory(certificates) - socket = socketFactory.createSocket(host, port) - } catch (e: GeneralSecurityException) { - throw IOException("Create SSL socket failed: ${e.message}") - } - } else { - socket = Socket() - val socketAddress: SocketAddress = InetSocketAddress(host, port) - socket.connect(socketAddress, timeout) - } - output = socket.getOutputStream().buffered() - input = socket.getInputStream().buffered() - socket.soTimeout = timeout + override suspend fun write(b: ByteArray) { + socket.write(b) } - override fun close() { - try { - if (socket.isConnected) { - socket.getInputStream().close() - input.close() - output.close() - socket.close() - } - } catch (ignored: Exception) {} + override suspend fun write(b: ByteArray, offset: Int, size: Int) { + socket.write(b, offset, size) } - override fun isConnected(): Boolean = socket.isConnected + override suspend fun writeUInt16(b: Int) { + socket.writeUInt16(b) + } - override fun isReachable(): Boolean = socket.inetAddress?.isReachable(5000) ?: false + override suspend fun writeUInt24(b: Int) { + socket.writeUInt24(b) + } + + override suspend fun writeUInt32(b: Int) { + socket.writeUInt32(b) + } + + override suspend fun writeUInt32LittleEndian(b: Int) { + socket.writeUInt32LittleEndian(b) + } + + override suspend fun read(): Int = socket.read() + + override suspend fun readUInt16(): Int = socket.readUInt16() + + override suspend fun readUInt24(): Int = socket.readUInt24() + + override suspend fun readUInt32(): Int = socket.readUInt32() + + override suspend fun readUInt32LittleEndian(): Int = socket.readUInt32LittleEndian() + + override suspend fun readUntil(b: ByteArray) { + socket.readUntil(b) + } } \ No newline at end of file diff --git a/rtmp/src/main/java/com/pedro/rtmp/utils/socket/TcpTunneledSocket.kt b/rtmp/src/main/java/com/pedro/rtmp/utils/socket/TcpTunneledSocket.kt index dd1568e07..181cdd789 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/utils/socket/TcpTunneledSocket.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/utils/socket/TcpTunneledSocket.kt @@ -18,6 +18,17 @@ package com.pedro.rtmp.utils.socket import android.util.Log import com.pedro.common.TimeUtils +import com.pedro.rtmp.utils.readUInt16 +import com.pedro.rtmp.utils.readUInt24 +import com.pedro.rtmp.utils.readUInt32 +import com.pedro.rtmp.utils.readUInt32LittleEndian +import com.pedro.rtmp.utils.readUntil +import com.pedro.rtmp.utils.writeUInt16 +import com.pedro.rtmp.utils.writeUInt24 +import com.pedro.rtmp.utils.writeUInt32 +import com.pedro.rtmp.utils.writeUInt32LittleEndian +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.* import java.net.HttpURLConnection import java.net.SocketTimeoutException @@ -36,6 +47,7 @@ class TcpTunneledSocket(private val host: String, private val port: Int, private "Content-Type" to "application/x-fcs", "User-Agent" to "Shockwave Flash" ) + private val timeout = 5000 private var connectionId: String = "" private var connected = false private var index = AtomicLong(0) @@ -46,9 +58,8 @@ class TcpTunneledSocket(private val host: String, private val port: Int, private //send video/audio packets in packs of 10 on each HTTP request. private val maxStoredPackets = 10 - override fun getOutStream(): OutputStream = output - - override fun getInputStream(): InputStream { + private fun getInputStream(requiredBytes: Int): InputStream { + if (input.available() >= requiredBytes) return input synchronized(sync) { val start = TimeUtils.getCurrentTimeMillis() while (input.available() <= 1 && connected) { @@ -63,7 +74,55 @@ class TcpTunneledSocket(private val host: String, private val port: Int, private return input } - override fun flush(isPacket: Boolean) { + @Throws(IOException::class) + private fun requestWrite(path: String, secured: Boolean, data: ByteArray) { + val socket = configureSocket(path, secured) + try { + socket.connect() + socket.outputStream.write(data) + val bytes = socket.inputStream.readBytes() + if (bytes.size > 1) input = ByteArrayInputStream(bytes, 1, bytes.size) + val success = socket.responseCode == HttpURLConnection.HTTP_OK + if (!success) throw IOException("send packet failed: ${socket.responseMessage}, broken pipe") + } finally { + socket.disconnect() + } + } + + @Throws(IOException::class) + private fun requestRead(path: String, secured: Boolean): ByteArray { + val socket = configureSocket(path, secured) + try { + socket.connect() + val data = socket.inputStream.readBytes() + val success = socket.responseCode == HttpURLConnection.HTTP_OK + if (!success) throw IOException("receive packet failed: ${socket.responseMessage}, broken pipe") + return data + } finally { + socket.disconnect() + } + } + + private fun configureSocket(path: String, secured: Boolean): HttpURLConnection { + val schema = if (secured) "https" else "http" + val url = URL("$schema://$host:$port/$path") + val socket = if (secured) { + url.openConnection() as HttpsURLConnection + } else { + url.openConnection() as HttpURLConnection + } + Log.i(TAG, "open: $url") + socket.requestMethod = "POST" + headers.forEach { (key, value) -> + socket.addRequestProperty(key, value) + } + socket.doOutput = true + socket.connectTimeout = timeout + socket.readTimeout = timeout + return socket + } + + override suspend fun flush(isPacket: Boolean) { synchronized(sync) { if (isPacket && storedPackets < maxStoredPackets) { storedPackets++ @@ -78,7 +137,7 @@ class TcpTunneledSocket(private val host: String, private val port: Int, private } } - override fun connect() { + override suspend fun connect() { synchronized(sync) { try { //optional in few servers @@ -97,7 +156,7 @@ class TcpTunneledSocket(private val host: String, private val port: Int, private } } - override fun close() { + override suspend fun close() { Log.i(TAG, "closing tunneled socket...") connected = false synchronized(sync) { @@ -119,51 +178,55 @@ class TcpTunneledSocket(private val host: String, private val port: Int, private override fun isReachable(): Boolean = connected - @Throws(IOException::class) - private fun requestWrite(path: String, secured: Boolean, data: ByteArray) { - val socket = configureSocket(path, secured) - try { - socket.connect() - socket.outputStream.write(data) - val bytes = socket.inputStream.readBytes() - if (bytes.size > 1) input = ByteArrayInputStream(bytes, 1, bytes.size) - val success = socket.responseCode == HttpURLConnection.HTTP_OK - if (!success) throw IOException("send packet failed: ${socket.responseMessage}") - } finally { - socket.disconnect() - } + override suspend fun write(b: Int) = withContext(Dispatchers.IO) { + output.write(b) } - @Throws(IOException::class) - private fun requestRead(path: String, secured: Boolean): ByteArray { - val socket = configureSocket(path, secured) - try { - socket.connect() - val data = socket.inputStream.readBytes() - val success = socket.responseCode == HttpURLConnection.HTTP_OK - if (!success) throw IOException("receive packet failed: ${socket.responseMessage}") - return data - } finally { - socket.disconnect() - } + override suspend fun write(b: ByteArray) = withContext(Dispatchers.IO) { + output.write(b) } - private fun configureSocket(path: String, secured: Boolean): HttpURLConnection { - val schema = if (secured) "https" else "http" - val url = URL("$schema://$host:$port/$path") - val socket = if (secured) { - url.openConnection() as HttpsURLConnection - } else { - url.openConnection() as HttpURLConnection - } - Log.i(TAG, "open: $url") - socket.requestMethod = "POST" - headers.forEach { (key, value) -> - socket.addRequestProperty(key, value) - } - socket.doOutput = true - socket.connectTimeout = timeout - socket.readTimeout = timeout - return socket + override suspend fun write(b: ByteArray, offset: Int, size: Int) { + output.write(b, offset, size) + } + + override suspend fun writeUInt16(b: Int) { + output.writeUInt16(b) + } + + override suspend fun writeUInt24(b: Int) { + output.writeUInt24(b) + } + + override suspend fun writeUInt32(b: Int) { + output.writeUInt32(b) + } + + override suspend fun writeUInt32LittleEndian(b: Int) { + output.writeUInt32LittleEndian(b) + } + + override suspend fun read(): Int = withContext(Dispatchers.IO) { + getInputStream(1).read() + } + + override suspend fun readUInt16(): Int { + return getInputStream(1).readUInt16() + } + + override suspend fun readUInt24(): Int { + return getInputStream(1).readUInt24() + } + + override suspend fun readUInt32(): Int { + return getInputStream(1).readUInt32() + } + + override suspend fun readUInt32LittleEndian(): Int { + return getInputStream(1).readUInt32LittleEndian() + } + + override suspend fun readUntil(b: ByteArray) { + return getInputStream(b.size).readUntil(b) } } \ No newline at end of file diff --git a/rtmp/src/test/java/com/pedro/rtmp/FakeRtmpSocket.kt b/rtmp/src/test/java/com/pedro/rtmp/FakeRtmpSocket.kt new file mode 100644 index 000000000..3704dfb29 --- /dev/null +++ b/rtmp/src/test/java/com/pedro/rtmp/FakeRtmpSocket.kt @@ -0,0 +1,103 @@ +/* + * + * * Copyright (C) 2024 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.rtmp + +import com.pedro.rtmp.utils.readUInt16 +import com.pedro.rtmp.utils.readUInt24 +import com.pedro.rtmp.utils.readUInt32 +import com.pedro.rtmp.utils.readUInt32LittleEndian +import com.pedro.rtmp.utils.readUntil +import com.pedro.rtmp.utils.socket.RtmpSocket +import com.pedro.rtmp.utils.writeUInt16 +import com.pedro.rtmp.utils.writeUInt24 +import com.pedro.rtmp.utils.writeUInt32 +import com.pedro.rtmp.utils.writeUInt32LittleEndian +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream + +/** + * Created by pedro on 24/9/24. + */ +class FakeRtmpSocket: RtmpSocket() { + + var input = ByteArrayInputStream(byteArrayOf()) + private set + val output = ByteArrayOutputStream() + private var connected = false + + fun setInputBytes(byteArray: ByteArray) { + input = ByteArrayInputStream(byteArray) + } + + override suspend fun flush(isPacket: Boolean) { + output.flush() + } + + override suspend fun connect() { + connected = true + } + + override suspend fun close() { + connected = false + } + + override fun isConnected(): Boolean = connected + + override fun isReachable(): Boolean = connected + + override suspend fun write(b: Int) { + output.write(b) + } + + override suspend fun write(b: ByteArray) { + output.write(b) + } + + override suspend fun write(b: ByteArray, offset: Int, size: Int) { + output.write(b, offset, size) + } + + override suspend fun writeUInt16(b: Int) { + output.writeUInt16(b) + } + + override suspend fun writeUInt24(b: Int) { + output.writeUInt24(b) + } + + override suspend fun writeUInt32(b: Int) { + output.writeUInt32(b) + } + + override suspend fun writeUInt32LittleEndian(b: Int) { + output.writeUInt32LittleEndian(b) + } + + override suspend fun read(): Int = input.read() + + override suspend fun readUInt16(): Int = input.readUInt16() + + override suspend fun readUInt24(): Int = input.readUInt24() + + override suspend fun readUInt32(): Int = input.readUInt32() + + override suspend fun readUInt32LittleEndian(): Int = input.readUInt32LittleEndian() + + override suspend fun readUntil(b: ByteArray) = input.readUntil(b) +} \ No newline at end of file diff --git a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/AbortTest.kt b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/AbortTest.kt index e3d926e2f..89f21204e 100644 --- a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/AbortTest.kt +++ b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/AbortTest.kt @@ -16,14 +16,15 @@ package com.pedro.rtmp.rtmp.message +import com.pedro.rtmp.FakeRtmpSocket import com.pedro.rtmp.utils.CommandSessionHistory import com.pedro.rtmp.utils.RtmpConfig +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream /** * Created by pedro on 10/9/23. @@ -31,27 +32,33 @@ import java.io.ByteArrayOutputStream class AbortTest { private val commandSessionHistory = CommandSessionHistory() + private lateinit var socket: FakeRtmpSocket + + @Before + fun setup() { + socket = FakeRtmpSocket() + } @Test - fun `GIVEN a buffer WHEN read rtmp message THEN get expected abort packet`() { + fun `GIVEN a buffer WHEN read rtmp message THEN get expected abort packet`() = runTest { val buffer = byteArrayOf(2, 0, 0, 0, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0) + socket.setInputBytes(buffer) val abort = Abort() - val message = RtmpMessage.getRtmpMessage(ByteArrayInputStream(buffer), RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) + val message = RtmpMessage.getRtmpMessage(socket, RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) assertTrue(message is Abort) assertEquals(abort.toString(), (message as Abort).toString()) } @Test - fun `GIVEN an abort packet WHEN write into a buffer THEN get expected buffer`() { + fun `GIVEN an abort packet WHEN write into a buffer THEN get expected buffer`() = runTest { val expectedBuffer = byteArrayOf(2, 0, 0, 0, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0) - val output = ByteArrayOutputStream() val abort = Abort() - abort.writeHeader(output) - abort.writeBody(output) + abort.writeHeader(socket) + abort.writeBody(socket) - assertArrayEquals(expectedBuffer, output.toByteArray()) + assertArrayEquals(expectedBuffer, socket.output.toByteArray()) } } \ No newline at end of file diff --git a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/AcknowledgementTest.kt b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/AcknowledgementTest.kt index d3c795a23..e77863975 100644 --- a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/AcknowledgementTest.kt +++ b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/AcknowledgementTest.kt @@ -16,14 +16,15 @@ package com.pedro.rtmp.rtmp.message +import com.pedro.rtmp.FakeRtmpSocket import com.pedro.rtmp.utils.CommandSessionHistory import com.pedro.rtmp.utils.RtmpConfig +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream /** * Created by pedro on 10/9/23. @@ -31,27 +32,33 @@ import java.io.ByteArrayOutputStream class AcknowledgementTest { private val commandSessionHistory = CommandSessionHistory() + private lateinit var socket: FakeRtmpSocket + + @Before + fun setup() { + socket = FakeRtmpSocket() + } @Test - fun `GIVEN a buffer WHEN read rtmp message THEN get expected acknowledgement packet`() { + fun `GIVEN a buffer WHEN read rtmp message THEN get expected acknowledgement packet`() = runTest { val buffer = byteArrayOf(2, 0, 0, 0, 0, 0, 4, 3, 0, 0, 0, 0, 0, 0, 0, 5) + socket.setInputBytes(buffer) val acknowledgement = Acknowledgement(5) - val message = RtmpMessage.getRtmpMessage(ByteArrayInputStream(buffer), RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) + val message = RtmpMessage.getRtmpMessage(socket, RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) assertTrue(message is Acknowledgement) assertEquals(acknowledgement.toString(), (message as Acknowledgement).toString()) } @Test - fun `GIVEN an acknowledgement packet WHEN write into a buffer THEN get expected buffer`() { + fun `GIVEN an acknowledgement packet WHEN write into a buffer THEN get expected buffer`() = runTest { val expectedBuffer = byteArrayOf(2, 0, 0, 0, 0, 0, 4, 3, 0, 0, 0, 0, 0, 0, 0, 5) - val output = ByteArrayOutputStream() val acknowledgement = Acknowledgement(5) - acknowledgement.writeHeader(output) - acknowledgement.writeBody(output) + acknowledgement.writeHeader(socket) + acknowledgement.writeBody(socket) - assertArrayEquals(expectedBuffer, output.toByteArray()) + assertArrayEquals(expectedBuffer, socket.output.toByteArray()) } } \ No newline at end of file diff --git a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/AudioVideoTest.kt b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/AudioVideoTest.kt index 441d4b705..8ff30a1ad 100644 --- a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/AudioVideoTest.kt +++ b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/AudioVideoTest.kt @@ -16,21 +16,29 @@ package com.pedro.rtmp.rtmp.message +import com.pedro.rtmp.FakeRtmpSocket import com.pedro.rtmp.flv.FlvPacket import com.pedro.rtmp.flv.FlvType +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals +import org.junit.Before import org.junit.Test -import java.io.ByteArrayOutputStream /** * Created by pedro on 10/9/23. */ class AudioVideoTest { + private lateinit var socket: FakeRtmpSocket + + @Before + fun setup() { + socket = FakeRtmpSocket() + } + @Test - fun `GIVEN an audio packet WHEN write into a buffer THEN get expected buffer`() { + fun `GIVEN an audio packet WHEN write into a buffer THEN get expected buffer`() = runTest { val expectedBuffer = byteArrayOf(7, 18, -42, -121, 0, 0, 100, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - val output = ByteArrayOutputStream() val fakePacket = FlvPacket( buffer = ByteArray(100) { 0x00 }, @@ -39,16 +47,15 @@ class AudioVideoTest { type = FlvType.AUDIO ) val audio = Audio(fakePacket) - audio.writeHeader(output) - audio.writeBody(output) + audio.writeHeader(socket) + audio.writeBody(socket) - assertArrayEquals(expectedBuffer, output.toByteArray()) + assertArrayEquals(expectedBuffer, socket.output.toByteArray()) } @Test - fun `GIVEN a video packet WHEN write into a buffer THEN get expected buffer`() { + fun `GIVEN a video packet WHEN write into a buffer THEN get expected buffer`() = runTest { val expectedBuffer = byteArrayOf(6, 18, -42, -121, 0, 0, 100, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - val output = ByteArrayOutputStream() val fakePacket = FlvPacket( buffer = ByteArray(100) { 0x00 }, @@ -57,9 +64,9 @@ class AudioVideoTest { type = FlvType.VIDEO ) val video = Video(fakePacket) - video.writeHeader(output) - video.writeBody(output) + video.writeHeader(socket) + video.writeBody(socket) - assertArrayEquals(expectedBuffer, output.toByteArray()) + assertArrayEquals(expectedBuffer, socket.output.toByteArray()) } } \ No newline at end of file diff --git a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/CommandTest.kt b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/CommandTest.kt index b1b8d27ff..7c65560eb 100644 --- a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/CommandTest.kt +++ b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/CommandTest.kt @@ -16,17 +16,18 @@ package com.pedro.rtmp.rtmp.message +import com.pedro.rtmp.FakeRtmpSocket import com.pedro.rtmp.amf.v0.AmfNumber import com.pedro.rtmp.amf.v0.AmfString import com.pedro.rtmp.rtmp.message.command.CommandAmf0 import com.pedro.rtmp.utils.CommandSessionHistory import com.pedro.rtmp.utils.RtmpConfig +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream /** * Created by pedro on 10/9/23. @@ -34,32 +35,38 @@ import java.io.ByteArrayOutputStream class CommandTest { private val commandSessionHistory = CommandSessionHistory() + private lateinit var socket: FakeRtmpSocket + + @Before + fun setup() { + socket = FakeRtmpSocket() + } @Test - fun `GIVEN a buffer WHEN read rtmp message THEN get expected command amf0 packet`() { + fun `GIVEN a buffer WHEN read rtmp message THEN get expected command amf0 packet`() = runTest { val buffer = byteArrayOf(3, 0, 0, 0, 0, 0, 34, 20, 0, 0, 0, 0, 2, 0, 4, 116, 101, 115, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 6, 114, 97, 110, 100, 111, 109, 0, 64, 52, 0, 0, 0, 0, 0, 0) + socket.setInputBytes(buffer) val commandAmf0 = CommandAmf0("test") commandAmf0.addData(AmfString("random")) commandAmf0.addData(AmfNumber(20.0)) - val message = RtmpMessage.getRtmpMessage(ByteArrayInputStream(buffer), RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) + val message = RtmpMessage.getRtmpMessage(socket, RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) assertTrue(message is CommandAmf0) assertEquals(commandAmf0.toString(), (message as CommandAmf0).toString()) } @Test - fun `GIVEN a command amf0 packet WHEN write into a buffer THEN get expected buffer`() { + fun `GIVEN a command amf0 packet WHEN write into a buffer THEN get expected buffer`() = runTest { val expectedBuffer = byteArrayOf(3, 0, 0, 0, 0, 0, 34, 20, 0, 0, 0, 0, 2, 0, 4, 116, 101, 115, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 6, 114, 97, 110, 100, 111, 109, 0, 64, 52, 0, 0, 0, 0, 0, 0) - val output = ByteArrayOutputStream() val commandAmf0 = CommandAmf0("test") commandAmf0.addData(AmfString("random")) commandAmf0.addData(AmfNumber(20.0)) - commandAmf0.writeHeader(output) - commandAmf0.writeBody(output) + commandAmf0.writeHeader(socket) + commandAmf0.writeBody(socket) - assertArrayEquals(expectedBuffer, output.toByteArray()) + assertArrayEquals(expectedBuffer, socket.output.toByteArray()) } } \ No newline at end of file diff --git a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/DataTest.kt b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/DataTest.kt index dcda431cc..d9d143762 100644 --- a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/DataTest.kt +++ b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/DataTest.kt @@ -16,17 +16,18 @@ package com.pedro.rtmp.rtmp.message +import com.pedro.rtmp.FakeRtmpSocket import com.pedro.rtmp.amf.v0.AmfNumber import com.pedro.rtmp.amf.v0.AmfString import com.pedro.rtmp.rtmp.message.data.DataAmf0 import com.pedro.rtmp.utils.CommandSessionHistory import com.pedro.rtmp.utils.RtmpConfig +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream /** * Created by pedro on 10/9/23. @@ -34,32 +35,38 @@ import java.io.ByteArrayOutputStream class DataTest { private val commandSessionHistory = CommandSessionHistory() + private lateinit var socket: FakeRtmpSocket + + @Before + fun setup() { + socket = FakeRtmpSocket() + } @Test - fun `GIVEN a buffer WHEN read rtmp message THEN get expected data amf0 packet`() { + fun `GIVEN a buffer WHEN read rtmp message THEN get expected data amf0 packet`() = runTest { val buffer = byteArrayOf(3, 0, 0, 0, 0, 0, 25, 18, 0, 0, 0, 0, 2, 0, 4, 116, 101, 115, 116, 2, 0, 6, 114, 97, 110, 100, 111, 109, 0, 64, 52, 0, 0, 0, 0, 0, 0) + socket.setInputBytes(buffer) val dataAmf0 = DataAmf0("test") dataAmf0.addData(AmfString("random")) dataAmf0.addData(AmfNumber(20.0)) - val message = RtmpMessage.getRtmpMessage(ByteArrayInputStream(buffer), RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) + val message = RtmpMessage.getRtmpMessage(socket, RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) assertTrue(message is DataAmf0) assertEquals(dataAmf0.toString(), (message as DataAmf0).toString()) } @Test - fun `GIVEN a data amf0 packet WHEN write into a buffer THEN get expected buffer`() { + fun `GIVEN a data amf0 packet WHEN write into a buffer THEN get expected buffer`() = runTest { val expectedBuffer = byteArrayOf(3, 0, 0, 0, 0, 0, 25, 18, 0, 0, 0, 0, 2, 0, 4, 116, 101, 115, 116, 2, 0, 6, 114, 97, 110, 100, 111, 109, 0, 64, 52, 0, 0, 0, 0, 0, 0) - val output = ByteArrayOutputStream() val dataAmf0 = DataAmf0("test") dataAmf0.addData(AmfString("random")) dataAmf0.addData(AmfNumber(20.0)) - dataAmf0.writeHeader(output) - dataAmf0.writeBody(output) + dataAmf0.writeHeader(socket) + dataAmf0.writeBody(socket) - assertArrayEquals(expectedBuffer, output.toByteArray()) + assertArrayEquals(expectedBuffer, socket.output.toByteArray()) } } \ No newline at end of file diff --git a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/SetChunkSizeTest.kt b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/SetChunkSizeTest.kt index a0289ea75..e7bdd35fe 100644 --- a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/SetChunkSizeTest.kt +++ b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/SetChunkSizeTest.kt @@ -16,14 +16,15 @@ package com.pedro.rtmp.rtmp.message +import com.pedro.rtmp.FakeRtmpSocket import com.pedro.rtmp.utils.CommandSessionHistory import com.pedro.rtmp.utils.RtmpConfig +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream /** * Created by pedro on 10/9/23. @@ -31,27 +32,33 @@ import java.io.ByteArrayOutputStream class SetChunkSizeTest { private val commandSessionHistory = CommandSessionHistory() + private lateinit var socket: FakeRtmpSocket + + @Before + fun setup() { + socket = FakeRtmpSocket() + } @Test - fun `GIVEN a buffer WHEN read rtmp message THEN get expected set chunk size packet`() { + fun `GIVEN a buffer WHEN read rtmp message THEN get expected set chunk size packet`() = runTest { val buffer = byteArrayOf(2, 0, 0, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 1, 0) + socket.setInputBytes(buffer) val setChunkSize = SetChunkSize(256) - val message = RtmpMessage.getRtmpMessage(ByteArrayInputStream(buffer), RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) + val message = RtmpMessage.getRtmpMessage(socket, RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) assertTrue(message is SetChunkSize) assertEquals(setChunkSize.toString(), (message as SetChunkSize).toString()) } @Test - fun `GIVEN a set chunk size packet WHEN write into a buffer THEN get expected buffer`() { + fun `GIVEN a set chunk size packet WHEN write into a buffer THEN get expected buffer`() = runTest { val expectedBuffer = byteArrayOf(2, 0, 0, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 1, 0) - val output = ByteArrayOutputStream() val setChunkSize = SetChunkSize(256) - setChunkSize.writeHeader(output) - setChunkSize.writeBody(output) + setChunkSize.writeHeader(socket) + setChunkSize.writeBody(socket) - assertArrayEquals(expectedBuffer, output.toByteArray()) + assertArrayEquals(expectedBuffer, socket.output.toByteArray()) } } \ No newline at end of file diff --git a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/SetPeerBandwidthTest.kt b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/SetPeerBandwidthTest.kt index e127bbdd5..7b08e14fc 100644 --- a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/SetPeerBandwidthTest.kt +++ b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/SetPeerBandwidthTest.kt @@ -16,14 +16,15 @@ package com.pedro.rtmp.rtmp.message +import com.pedro.rtmp.FakeRtmpSocket import com.pedro.rtmp.utils.CommandSessionHistory import com.pedro.rtmp.utils.RtmpConfig +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream /** * Created by pedro on 10/9/23. @@ -31,27 +32,33 @@ import java.io.ByteArrayOutputStream class SetPeerBandwidthTest { private val commandSessionHistory = CommandSessionHistory() + private lateinit var socket: FakeRtmpSocket + + @Before + fun setup() { + socket = FakeRtmpSocket() + } @Test - fun `GIVEN a buffer WHEN read rtmp message THEN get expected set peer bandwidth packet`() { + fun `GIVEN a buffer WHEN read rtmp message THEN get expected set peer bandwidth packet`() = runTest { val buffer = byteArrayOf(2, 0, 0, 0, 0, 0, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 2) + socket.setInputBytes(buffer) val setPeerBandwidth = SetPeerBandwidth() - val message = RtmpMessage.getRtmpMessage(ByteArrayInputStream(buffer), RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) + val message = RtmpMessage.getRtmpMessage(socket, RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) assertTrue(message is SetPeerBandwidth) assertEquals(setPeerBandwidth.toString(), (message as SetPeerBandwidth).toString()) } @Test - fun `GIVEN a set peer bandwidth packet WHEN write into a buffer THEN get expected buffer`() { + fun `GIVEN a set peer bandwidth packet WHEN write into a buffer THEN get expected buffer`() = runTest { val expectedBuffer = byteArrayOf(2, 0, 0, 0, 0, 0, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 2) - val output = ByteArrayOutputStream() val setPeerBandwidth = SetPeerBandwidth() - setPeerBandwidth.writeHeader(output) - setPeerBandwidth.writeBody(output) + setPeerBandwidth.writeHeader(socket) + setPeerBandwidth.writeBody(socket) - assertArrayEquals(expectedBuffer, output.toByteArray()) + assertArrayEquals(expectedBuffer, socket.output.toByteArray()) } } \ No newline at end of file diff --git a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/UserControlTest.kt b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/UserControlTest.kt index e33851693..991c42134 100644 --- a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/UserControlTest.kt +++ b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/UserControlTest.kt @@ -16,15 +16,16 @@ package com.pedro.rtmp.rtmp.message +import com.pedro.rtmp.FakeRtmpSocket import com.pedro.rtmp.rtmp.message.control.UserControl import com.pedro.rtmp.utils.CommandSessionHistory import com.pedro.rtmp.utils.RtmpConfig +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream /** * Created by pedro on 10/9/23. @@ -32,27 +33,33 @@ import java.io.ByteArrayOutputStream class UserControlTest { private val commandSessionHistory = CommandSessionHistory() + private lateinit var socket: FakeRtmpSocket + + @Before + fun setup() { + socket = FakeRtmpSocket() + } @Test - fun `GIVEN a buffer WHEN read rtmp message THEN get expected user control packet`() { + fun `GIVEN a buffer WHEN read rtmp message THEN get expected user control packet`() = runTest { val buffer = byteArrayOf(2, 0, 0, 0, 0, 0, 6, 4, 0, 0, 0, 0, 0, 6, -1, -1, -1, -1) + socket.setInputBytes(buffer) val userControl = UserControl() - val message = RtmpMessage.getRtmpMessage(ByteArrayInputStream(buffer), RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) + val message = RtmpMessage.getRtmpMessage(socket, RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) assertTrue(message is UserControl) assertEquals(userControl.toString(), (message as UserControl).toString()) } @Test - fun `GIVEN an user control packet WHEN write into a buffer THEN get expected buffer`() { + fun `GIVEN an user control packet WHEN write into a buffer THEN get expected buffer`() = runTest { val expectedBuffer = byteArrayOf(2, 0, 0, 0, 0, 0, 6, 4, 0, 0, 0, 0, 0, 6, -1, -1, -1, -1) - val output = ByteArrayOutputStream() val userControl = UserControl() - userControl.writeHeader(output) - userControl.writeBody(output) + userControl.writeHeader(socket) + userControl.writeBody(socket) - assertArrayEquals(expectedBuffer, output.toByteArray()) + assertArrayEquals(expectedBuffer, socket.output.toByteArray()) } } \ No newline at end of file diff --git a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/WindowAcknowledgementSizeTest.kt b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/WindowAcknowledgementSizeTest.kt index cf48ce7e2..2d5b4b7c2 100644 --- a/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/WindowAcknowledgementSizeTest.kt +++ b/rtmp/src/test/java/com/pedro/rtmp/rtmp/message/WindowAcknowledgementSizeTest.kt @@ -16,14 +16,15 @@ package com.pedro.rtmp.rtmp.message +import com.pedro.rtmp.FakeRtmpSocket import com.pedro.rtmp.utils.CommandSessionHistory import com.pedro.rtmp.utils.RtmpConfig +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream /** * Created by pedro on 10/9/23. @@ -31,27 +32,33 @@ import java.io.ByteArrayOutputStream class WindowAcknowledgementSizeTest { private val commandSessionHistory = CommandSessionHistory() + private lateinit var socket: FakeRtmpSocket + + @Before + fun setup() { + socket = FakeRtmpSocket() + } @Test - fun `GIVEN a buffer WHEN read rtmp message THEN get expected window acknowledgement size packet`() { + fun `GIVEN a buffer WHEN read rtmp message THEN get expected window acknowledgement size packet`() = runTest { val buffer = byteArrayOf(2, 18, -42, -121, 0, 0, 4, 5, 0, 0, 0, 0, 0, 0, 1, 0) + socket.setInputBytes(buffer) val windowAcknowledgementSize = WindowAcknowledgementSize(256, 1234567) - val message = RtmpMessage.getRtmpMessage(ByteArrayInputStream(buffer), RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) + val message = RtmpMessage.getRtmpMessage(socket, RtmpConfig.DEFAULT_CHUNK_SIZE, commandSessionHistory) assertTrue(message is WindowAcknowledgementSize) assertEquals(windowAcknowledgementSize.toString(), (message as WindowAcknowledgementSize).toString()) } @Test - fun `GIVEN a window acknowledgement size packet WHEN write into a buffer THEN get expected buffer`() { + fun `GIVEN a window acknowledgement size packet WHEN write into a buffer THEN get expected buffer`() = runTest { val expectedBuffer = byteArrayOf(2, 18, -42, -121, 0, 0, 4, 5, 0, 0, 0, 0, 0, 0, 1, 0) - val output = ByteArrayOutputStream() val windowAcknowledgementSize = WindowAcknowledgementSize(256, 1234567) - windowAcknowledgementSize.writeHeader(output) - windowAcknowledgementSize.writeBody(output) + windowAcknowledgementSize.writeHeader(socket) + windowAcknowledgementSize.writeBody(socket) - assertArrayEquals(expectedBuffer, output.toByteArray()) + assertArrayEquals(expectedBuffer, socket.output.toByteArray()) } } \ No newline at end of file diff --git a/rtmp/src/test/java/com/pedro/rtmp/utils/SocketTest.kt b/rtmp/src/test/java/com/pedro/rtmp/utils/SocketTest.kt deleted file mode 100644 index 4fd759071..000000000 --- a/rtmp/src/test/java/com/pedro/rtmp/utils/SocketTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2024 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.rtmp.utils - -import com.pedro.rtmp.utils.socket.TcpSocket -import org.junit.Test - -class SocketTest { - - @Test - fun `check tcp socket error with socket not connected`() { - val socket = TcpSocket("127.0.0.1", 1935, false, null) - socket.getOutStream().write(0) - socket.getInputStream() - socket.close() - } -} \ No newline at end of file diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtcp/BaseSenderReport.kt b/rtsp/src/main/java/com/pedro/rtsp/rtcp/BaseSenderReport.kt index fa95e34c9..48014dc00 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtcp/BaseSenderReport.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtcp/BaseSenderReport.kt @@ -17,12 +17,13 @@ package com.pedro.rtsp.rtcp import com.pedro.common.TimeUtils +import com.pedro.common.socket.TcpStreamSocket +import com.pedro.common.socket.UdpStreamSocket import com.pedro.rtsp.rtsp.Protocol import com.pedro.rtsp.rtsp.RtpFrame import com.pedro.rtsp.utils.RtpConstants import com.pedro.rtsp.utils.setLong import java.io.IOException -import java.io.OutputStream /** * Created by pedro on 7/11/18. @@ -30,8 +31,8 @@ import java.io.OutputStream abstract class BaseSenderReport internal constructor() { private val interval: Long = 3000 - private val videoBuffer = ByteArray(RtpConstants.MTU) - private val audioBuffer = ByteArray(RtpConstants.MTU) + private val videoBuffer = ByteArray(RtpConstants.REPORT_PACKET_LENGTH) + private val audioBuffer = ByteArray(RtpConstants.REPORT_PACKET_LENGTH) private var videoTime: Long = 0 private var audioTime: Long = 0 private var videoPacketCount = 0L @@ -41,11 +42,21 @@ abstract class BaseSenderReport internal constructor() { companion object { @JvmStatic - fun getInstance(protocol: Protocol, videoSourcePort: Int, audioSourcePort: Int): BaseSenderReport { + fun getInstance( + protocol: Protocol, host: String, + videoSourcePort: Int, audioSourcePort: Int, + videoServerPort: Int, audioServerPort: Int, + ): BaseSenderReport { return if (protocol === Protocol.TCP) { SenderReportTcp() } else { - SenderReportUdp(videoSourcePort, audioSourcePort) + val videoSocket = UdpStreamSocket( + host, videoServerPort, videoSourcePort, receiveSize = RtpConstants.REPORT_PACKET_LENGTH + ) + val audioSocket = UdpStreamSocket( + host, audioServerPort, audioSourcePort, receiveSize = RtpConstants.REPORT_PACKET_LENGTH + ) + SenderReportUdp(videoSocket, audioSocket) } } } @@ -82,7 +93,7 @@ abstract class BaseSenderReport internal constructor() { } @Throws(IOException::class) - abstract fun setDataStream(outputStream: OutputStream, host: String) + abstract suspend fun setSocket(socket: TcpStreamSocket) @Throws(IOException::class) suspend fun update(rtpFrame: RtpFrame): Boolean { @@ -139,7 +150,7 @@ abstract class BaseSenderReport internal constructor() { audioBuffer.setLong(audioOctetCount, 24, 28) } - abstract fun close() + abstract suspend fun close() private fun setData(buffer: ByteArray, ntpts: Long, rtpts: Long) { val hb = ntpts / 1000000000 diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtcp/SenderReportTcp.kt b/rtsp/src/main/java/com/pedro/rtsp/rtcp/SenderReportTcp.kt index 11e55f299..9b9cd6242 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtcp/SenderReportTcp.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtcp/SenderReportTcp.kt @@ -16,22 +16,22 @@ package com.pedro.rtsp.rtcp +import com.pedro.common.socket.TcpStreamSocket import com.pedro.rtsp.rtsp.RtpFrame import com.pedro.rtsp.utils.RtpConstants import java.io.IOException -import java.io.OutputStream /** * Created by pedro on 8/11/18. */ class SenderReportTcp : BaseSenderReport() { - private var outputStream: OutputStream? = null + private var socket: TcpStreamSocket? = null private val tcpHeader: ByteArray = byteArrayOf('$'.code.toByte(), 0, 0, RtpConstants.REPORT_PACKET_LENGTH.toByte()) @Throws(IOException::class) - override fun setDataStream(outputStream: OutputStream, host: String) { - this.outputStream = outputStream + override suspend fun setSocket(socket: TcpStreamSocket) { + this.socket = socket } @Throws(IOException::class) @@ -39,15 +39,13 @@ class SenderReportTcp : BaseSenderReport() { sendReportTCP(buffer, rtpFrame.channelIdentifier) } - override fun close() {} + override suspend fun close() {} @Throws(IOException::class) - private fun sendReportTCP(buffer: ByteArray, channelIdentifier: Int) { - synchronized(RtpConstants.lock) { - tcpHeader[1] = (2 * channelIdentifier + 1).toByte() - outputStream?.write(tcpHeader) - outputStream?.write(buffer, 0, RtpConstants.REPORT_PACKET_LENGTH) - outputStream?.flush() - } + private suspend fun sendReportTCP(buffer: ByteArray, channelIdentifier: Int) { + tcpHeader[1] = (2 * channelIdentifier + 1).toByte() + socket?.write(tcpHeader) + socket?.write(buffer, 0, RtpConstants.REPORT_PACKET_LENGTH) + socket?.flush() } } \ No newline at end of file diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtcp/SenderReportUdp.kt b/rtsp/src/main/java/com/pedro/rtsp/rtcp/SenderReportUdp.kt index 94319e699..4f9125449 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtcp/SenderReportUdp.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtcp/SenderReportUdp.kt @@ -16,58 +16,41 @@ package com.pedro.rtsp.rtcp +import com.pedro.common.socket.TcpStreamSocket +import com.pedro.common.socket.UdpStreamSocket import com.pedro.rtsp.rtsp.RtpFrame -import com.pedro.rtsp.utils.RtpConstants import java.io.IOException -import java.io.OutputStream -import java.net.DatagramPacket -import java.net.InetAddress -import java.net.MulticastSocket /** * Created by pedro on 8/11/18. */ class SenderReportUdp( - videoSourcePort: Int, audioSourcePort: Int, - private var multicastSocketVideo: MulticastSocket? = null, - private var multicastSocketAudio: MulticastSocket? = null + private val videoSocket: UdpStreamSocket, + private val audioSocket: UdpStreamSocket, ) : BaseSenderReport() { - private val datagramPacket = DatagramPacket(byteArrayOf(0), 1) - - init { - if (multicastSocketVideo == null) multicastSocketVideo = MulticastSocket(videoSourcePort) - multicastSocketVideo?.timeToLive = 64 - if (multicastSocketAudio == null) multicastSocketAudio = MulticastSocket(audioSourcePort) - multicastSocketAudio?.timeToLive = 64 - } - @Throws(IOException::class) - override fun setDataStream(outputStream: OutputStream, host: String) { - datagramPacket.address = InetAddress.getByName(host) + override suspend fun setSocket(socket: TcpStreamSocket) { + videoSocket.connect() + audioSocket.connect() } @Throws(IOException::class) override suspend fun sendReport(buffer: ByteArray, rtpFrame: RtpFrame) { - sendReportUDP(buffer, rtpFrame.rtcpPort, rtpFrame.isVideoFrame()) + sendReportUDP(buffer, rtpFrame.isVideoFrame()) } - override fun close() { - multicastSocketVideo?.close() - multicastSocketAudio?.close() + override suspend fun close() { + videoSocket.close() + audioSocket.close() } @Throws(IOException::class) - private fun sendReportUDP(buffer: ByteArray, port: Int, isVideo: Boolean) { - synchronized(RtpConstants.lock) { - datagramPacket.data = buffer - datagramPacket.port = port - datagramPacket.length = RtpConstants.REPORT_PACKET_LENGTH - if (isVideo) { - multicastSocketVideo?.send(datagramPacket) - } else { - multicastSocketAudio?.send(datagramPacket) - } + private suspend fun sendReportUDP(buffer: ByteArray, isVideo: Boolean) { + if (isVideo) { + videoSocket.writePacket(buffer) + } else { + audioSocket.writePacket(buffer) } } } \ No newline at end of file diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/AacPacket.kt b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/AacPacket.kt index f282626f3..3d2947154 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/AacPacket.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/AacPacket.kt @@ -72,7 +72,7 @@ class AacPacket( buffer[RtpConstants.RTP_HEADER_LENGTH + 3] = buffer[RtpConstants.RTP_HEADER_LENGTH + 3] and 0xF8.toByte() buffer[RtpConstants.RTP_HEADER_LENGTH + 3] = buffer[RtpConstants.RTP_HEADER_LENGTH + 3] or 0x00 updateSeq(buffer) - val rtpFrame = RtpFrame(buffer, rtpTs, RtpConstants.RTP_HEADER_LENGTH + size + 4, rtpPort, rtcpPort, channelIdentifier) + val rtpFrame = RtpFrame(buffer, rtpTs, RtpConstants.RTP_HEADER_LENGTH + size + 4, channelIdentifier) sum += size frames.add(rtpFrame) } diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/Av1Packet.kt b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/Av1Packet.kt index df1d41cdf..a1d1df5ca 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/Av1Packet.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/Av1Packet.kt @@ -96,7 +96,7 @@ class Av1Packet: BasePacket( val oSize = if (isFirstPacket) obuList.size else 1 buffer[RtpConstants.RTP_HEADER_LENGTH] = generateAv1AggregationHeader(bufferInfo.isKeyframe(), isFirstPacket, isLastPacket, oSize) updateSeq(buffer) - val rtpFrame = RtpFrame(buffer, rtpTs, buffer.size, rtpPort, rtcpPort, channelIdentifier) + val rtpFrame = RtpFrame(buffer, rtpTs, buffer.size, channelIdentifier) frames.add(rtpFrame) } callback(frames) diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/BasePacket.kt b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/BasePacket.kt index 6bc297f86..a932a8faf 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/BasePacket.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/BasePacket.kt @@ -30,8 +30,6 @@ import kotlin.experimental.or abstract class BasePacket(private val clock: Long, private val payloadType: Int) { protected var channelIdentifier: Int = 0 - protected var rtpPort = 0 - protected var rtcpPort = 0 private var seq = 0L private var ssrc = 0L protected val maxPacketSize = RtpConstants.MTU - 28 @@ -43,11 +41,6 @@ abstract class BasePacket(private val clock: Long, private val payloadType: Int) callback: (List) -> Unit ) - fun setPorts(rtpPort: Int, rtcpPort: Int) { - this.rtpPort = rtpPort - this.rtcpPort = rtcpPort - } - open fun reset() { seq = 0 ssrc = 0 diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/G711Packet.kt b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/G711Packet.kt index 3ef9747d7..37be354c9 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/G711Packet.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/G711Packet.kt @@ -54,7 +54,7 @@ class G711Packet( markPacket(buffer) val rtpTs = updateTimeStamp(buffer, ts) updateSeq(buffer) - val rtpFrame = RtpFrame(buffer, rtpTs, RtpConstants.RTP_HEADER_LENGTH + size , rtpPort, rtcpPort, channelIdentifier) + val rtpFrame = RtpFrame(buffer, rtpTs, RtpConstants.RTP_HEADER_LENGTH + size , channelIdentifier) sum += size frames.add(rtpFrame) } diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/H264Packet.kt b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/H264Packet.kt index b4ffd3888..a84465418 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/H264Packet.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/H264Packet.kt @@ -71,7 +71,7 @@ class H264Packet( markPacket(buffer) //mark end frame System.arraycopy(it, 0, buffer, RtpConstants.RTP_HEADER_LENGTH, it.size) updateSeq(buffer) - val rtpFrame = RtpFrame(buffer, rtpTs, it.size + RtpConstants.RTP_HEADER_LENGTH, rtpPort, rtcpPort, channelIdentifier) + val rtpFrame = RtpFrame(buffer, rtpTs, it.size + RtpConstants.RTP_HEADER_LENGTH, channelIdentifier) frames.add(rtpFrame) sendKeyFrame = true } ?: run { @@ -87,7 +87,7 @@ class H264Packet( val rtpTs = updateTimeStamp(buffer, ts) markPacket(buffer) //mark end frame updateSeq(buffer) - val rtpFrame = RtpFrame(buffer, rtpTs, buffer.size, rtpPort, rtcpPort, channelIdentifier) + val rtpFrame = RtpFrame(buffer, rtpTs, buffer.size, channelIdentifier) frames.add(rtpFrame) } else { // Set FU-A header @@ -116,7 +116,7 @@ class H264Packet( markPacket(buffer) //mark end frame } updateSeq(buffer) - val rtpFrame = RtpFrame(buffer, rtpTs, buffer.size, rtpPort, rtcpPort, channelIdentifier) + val rtpFrame = RtpFrame(buffer, rtpTs, buffer.size, channelIdentifier) frames.add(rtpFrame) // Switch start bit header[1] = header[1] and 0x7F diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/H265Packet.kt b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/H265Packet.kt index abbb32d84..6b3e6a21d 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/H265Packet.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/H265Packet.kt @@ -63,7 +63,7 @@ class H265Packet: BasePacket( val rtpTs = updateTimeStamp(buffer, ts) markPacket(buffer) //mark end frame updateSeq(buffer) - val rtpFrame = RtpFrame(buffer, rtpTs, buffer.size, rtpPort, rtcpPort, channelIdentifier) + val rtpFrame = RtpFrame(buffer, rtpTs, buffer.size, channelIdentifier) frames.add(rtpFrame) } else { //Set PayloadHdr (16bit type=49) @@ -98,7 +98,7 @@ class H265Packet: BasePacket( markPacket(buffer) //mark end frame } updateSeq(buffer) - val rtpFrame = RtpFrame(buffer, rtpTs, buffer.size, rtpPort, rtcpPort, channelIdentifier) + val rtpFrame = RtpFrame(buffer, rtpTs, buffer.size, channelIdentifier) frames.add(rtpFrame) // Switch start bit header[2] = header[2] and 0x7F diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/OpusPacket.kt b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/OpusPacket.kt index cab19320c..e6e4441c0 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/OpusPacket.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtp/packets/OpusPacket.kt @@ -54,7 +54,7 @@ class OpusPacket( markPacket(buffer) val rtpTs = updateTimeStamp(buffer, ts) updateSeq(buffer) - val rtpFrame = RtpFrame(buffer, rtpTs, RtpConstants.RTP_HEADER_LENGTH + size , rtpPort, rtcpPort, channelIdentifier) + val rtpFrame = RtpFrame(buffer, rtpTs, RtpConstants.RTP_HEADER_LENGTH + size , channelIdentifier) sum += size frames.add(rtpFrame) } diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtp/sockets/BaseRtpSocket.kt b/rtsp/src/main/java/com/pedro/rtsp/rtp/sockets/BaseRtpSocket.kt index 599686c8b..1aba6f48e 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtp/sockets/BaseRtpSocket.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtp/sockets/BaseRtpSocket.kt @@ -16,10 +16,12 @@ package com.pedro.rtsp.rtp.sockets +import com.pedro.common.socket.TcpStreamSocket +import com.pedro.common.socket.UdpStreamSocket import com.pedro.rtsp.rtsp.Protocol import com.pedro.rtsp.rtsp.RtpFrame +import com.pedro.rtsp.utils.RtpConstants import java.io.IOException -import java.io.OutputStream /** * Created by pedro on 7/11/18. @@ -28,20 +30,30 @@ abstract class BaseRtpSocket { companion object { @JvmStatic - fun getInstance(protocol: Protocol, videoSourcePort: Int, audioSourcePort: Int): BaseRtpSocket { + fun getInstance( + protocol: Protocol, host: String, + videoSourcePort: Int, audioSourcePort: Int, + videoServerPort: Int, audioServerPort: Int, + ): BaseRtpSocket { return if (protocol === Protocol.TCP) { RtpSocketTcp() } else { - RtpSocketUdp(videoSourcePort, audioSourcePort) + val videoSocket = UdpStreamSocket( + host, videoServerPort, videoSourcePort, receiveSize = RtpConstants.MTU + ) + val audioSocket = UdpStreamSocket( + host, audioServerPort, audioSourcePort, receiveSize = RtpConstants.MTU + ) + RtpSocketUdp(videoSocket, audioSocket) } } } @Throws(IOException::class) - abstract fun setDataStream(outputStream: OutputStream, host: String) + abstract suspend fun setSocket(socket: TcpStreamSocket) @Throws(IOException::class) abstract suspend fun sendFrame(rtpFrame: RtpFrame) - abstract fun close() + abstract suspend fun close() } \ No newline at end of file diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtp/sockets/RtpSocketTcp.kt b/rtsp/src/main/java/com/pedro/rtsp/rtp/sockets/RtpSocketTcp.kt index 7c629af74..7cf44579d 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtp/sockets/RtpSocketTcp.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtp/sockets/RtpSocketTcp.kt @@ -16,22 +16,21 @@ package com.pedro.rtsp.rtp.sockets +import com.pedro.common.socket.TcpStreamSocket import com.pedro.rtsp.rtsp.RtpFrame -import com.pedro.rtsp.utils.RtpConstants import java.io.IOException -import java.io.OutputStream /** * Created by pedro on 7/11/18. */ class RtpSocketTcp : BaseRtpSocket() { - private var outputStream: OutputStream? = null + private var socket: TcpStreamSocket? = null private val tcpHeader: ByteArray = byteArrayOf('$'.code.toByte(), 0, 0, 0) @Throws(IOException::class) - override fun setDataStream(outputStream: OutputStream, host: String) { - this.outputStream = outputStream + override suspend fun setSocket(socket: TcpStreamSocket) { + this.socket = socket } @Throws(IOException::class) @@ -39,18 +38,16 @@ class RtpSocketTcp : BaseRtpSocket() { sendFrameTCP(rtpFrame) } - override fun close() {} + override suspend fun close() {} @Throws(IOException::class) - private fun sendFrameTCP(rtpFrame: RtpFrame) { - synchronized(RtpConstants.lock) { - val len = rtpFrame.length - tcpHeader[1] = (2 * rtpFrame.channelIdentifier).toByte() - tcpHeader[2] = (len shr 8).toByte() - tcpHeader[3] = (len and 0xFF).toByte() - outputStream?.write(tcpHeader) - outputStream?.write(rtpFrame.buffer, 0, len) - outputStream?.flush() - } + private suspend fun sendFrameTCP(rtpFrame: RtpFrame) { + val len = rtpFrame.length + tcpHeader[1] = (2 * rtpFrame.channelIdentifier).toByte() + tcpHeader[2] = (len shr 8).toByte() + tcpHeader[3] = (len and 0xFF).toByte() + socket?.write(tcpHeader) + socket?.write(rtpFrame.buffer, 0, len) + socket?.flush() } } \ No newline at end of file diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtp/sockets/RtpSocketUdp.kt b/rtsp/src/main/java/com/pedro/rtsp/rtp/sockets/RtpSocketUdp.kt index 3eb688629..25b7ddb72 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtp/sockets/RtpSocketUdp.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtp/sockets/RtpSocketUdp.kt @@ -16,35 +16,23 @@ package com.pedro.rtsp.rtp.sockets +import com.pedro.common.socket.TcpStreamSocket +import com.pedro.common.socket.UdpStreamSocket import com.pedro.rtsp.rtsp.RtpFrame -import com.pedro.rtsp.utils.RtpConstants import java.io.IOException -import java.io.OutputStream -import java.net.DatagramPacket -import java.net.InetAddress -import java.net.MulticastSocket /** * Created by pedro on 7/11/18. */ class RtpSocketUdp( - videoSourcePort: Int, audioSourcePort: Int, - private var multicastSocketVideo: MulticastSocket? = null, - private var multicastSocketAudio: MulticastSocket? = null + private val videoSocket: UdpStreamSocket, + private val audioSocket: UdpStreamSocket, ) : BaseRtpSocket() { - private val datagramPacket = DatagramPacket(byteArrayOf(0), 1) - - init { - if (multicastSocketVideo == null) multicastSocketVideo = MulticastSocket(videoSourcePort) - multicastSocketVideo?.timeToLive = 64 - if (multicastSocketAudio == null) multicastSocketAudio = MulticastSocket(audioSourcePort) - multicastSocketAudio?.timeToLive = 64 - } - @Throws(IOException::class) - override fun setDataStream(outputStream: OutputStream, host: String) { - datagramPacket.address = InetAddress.getByName(host) + override suspend fun setSocket(socket: TcpStreamSocket) { + videoSocket.connect() + audioSocket.connect() } @Throws(IOException::class) @@ -52,22 +40,17 @@ class RtpSocketUdp( sendFrameUDP(rtpFrame) } - override fun close() { - multicastSocketVideo?.close() - multicastSocketAudio?.close() + override suspend fun close() { + videoSocket.close() + audioSocket.close() } @Throws(IOException::class) - private fun sendFrameUDP(rtpFrame: RtpFrame) { - synchronized(RtpConstants.lock) { - datagramPacket.data = rtpFrame.buffer - datagramPacket.port = rtpFrame.rtpPort - datagramPacket.length = rtpFrame.length - if (rtpFrame.isVideoFrame()) { - multicastSocketVideo?.send(datagramPacket) - } else { - multicastSocketAudio?.send(datagramPacket) - } + private suspend fun sendFrameUDP(rtpFrame: RtpFrame) { + if (rtpFrame.isVideoFrame()) { + videoSocket.writePacket(rtpFrame.buffer) + } else { + audioSocket.writePacket(rtpFrame.buffer) } } } \ No newline at end of file diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtpFrame.kt b/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtpFrame.kt index b98d62c92..c1120acfa 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtpFrame.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtpFrame.kt @@ -21,8 +21,7 @@ import com.pedro.rtsp.utils.RtpConstants /** * Created by pedro on 7/11/18. */ -data class RtpFrame(val buffer: ByteArray, val timeStamp: Long, val length: Int, - val rtpPort: Int, val rtcpPort: Int, val channelIdentifier: Int) { +data class RtpFrame(val buffer: ByteArray, val timeStamp: Long, val length: Int, val channelIdentifier: Int) { fun isVideoFrame(): Boolean = channelIdentifier == RtpConstants.trackVideo @@ -35,8 +34,6 @@ data class RtpFrame(val buffer: ByteArray, val timeStamp: Long, val length: Int, if (!buffer.contentEquals(other.buffer)) return false if (timeStamp != other.timeStamp) return false if (length != other.length) return false - if (rtpPort != other.rtpPort) return false - if (rtcpPort != other.rtcpPort) return false if (channelIdentifier != other.channelIdentifier) return false return true @@ -46,8 +43,6 @@ data class RtpFrame(val buffer: ByteArray, val timeStamp: Long, val length: Int, var result = buffer.contentHashCode() result = 31 * result + timeStamp.hashCode() result = 31 * result + length - result = 31 * result + rtpPort - result = 31 * result + rtcpPort result = 31 * result + channelIdentifier return result } diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtspClient.kt b/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtspClient.kt index e108ee4a9..34c90db3f 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtspClient.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtspClient.kt @@ -20,10 +20,12 @@ import android.media.MediaCodec import android.util.Log import com.pedro.common.AudioCodec import com.pedro.common.ConnectChecker -import com.pedro.common.TLSSocketFactory +import com.pedro.common.ConnectionFailed import com.pedro.common.UrlParser import com.pedro.common.VideoCodec import com.pedro.common.onMainThread +import com.pedro.common.socket.TcpStreamSocket +import com.pedro.common.validMessage import com.pedro.rtsp.rtsp.commands.CommandsManager import com.pedro.rtsp.rtsp.commands.Method import com.pedro.rtsp.utils.RtpConstants @@ -38,13 +40,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withTimeoutOrNull import java.io.* -import java.net.InetSocketAddress -import java.net.Socket -import java.net.SocketAddress -import java.net.SocketTimeoutException import java.net.URISyntaxException import java.nio.ByteBuffer -import java.security.GeneralSecurityException import javax.net.ssl.TrustManager /** @@ -57,9 +54,7 @@ class RtspClient(private val connectChecker: ConnectChecker) { private val validSchemes = arrayOf("rtsp", "rtsps") //sockets objects - private var connectionSocket: Socket? = null - private var reader: BufferedReader? = null - private var writer: BufferedWriter? = null + private var socket: TcpStreamSocket? = null private var scope = CoroutineScope(Dispatchers.IO) private var scopeRetry = CoroutineScope(Dispatchers.IO) private var job: Job? = null @@ -72,7 +67,7 @@ class RtspClient(private val connectChecker: ConnectChecker) { //for secure transport private var tlsEnabled = false - private var certificates: Array? = null + private var certificates: TrustManager? = null private val commandsManager: CommandsManager = CommandsManager() private val rtspSender: RtspSender = RtspSender(connectChecker, commandsManager) private var url: String? = null @@ -96,7 +91,7 @@ class RtspClient(private val connectChecker: ConnectChecker) { /** * Add certificates for TLS connection */ - fun addCertificates(certificates: Array?) { + fun addCertificates(certificates: TrustManager?) { this.certificates = certificates } @@ -222,8 +217,12 @@ class RtspClient(private val connectChecker: ConnectChecker) { val error = runCatching { commandsManager.setUrl(host, port, "/$path") rtspSender.setSocketsInfo(commandsManager.protocol, + host, commandsManager.videoClientPorts, - commandsManager.audioClientPorts) + commandsManager.audioClientPorts, + commandsManager.videoServerPorts, + commandsManager.audioServerPorts + ) if (!commandsManager.audioDisabled) { rtspSender.setAudioInfo(commandsManager.sampleRate) } @@ -242,31 +241,16 @@ class RtspClient(private val connectChecker: ConnectChecker) { } rtspSender.setVideoInfo(commandsManager.sps!!, commandsManager.pps, commandsManager.vps) } - if (!tlsEnabled) { - connectionSocket = Socket() - val socketAddress: SocketAddress = InetSocketAddress(host, port) - connectionSocket?.connect(socketAddress, 5000) - } else { - try { - val socketFactory = TLSSocketFactory(certificates) - connectionSocket = socketFactory.createSocket(host, port) - } catch (e: GeneralSecurityException) { - throw IOException("Create SSL socket failed: ${e.message}") - } - } - connectionSocket?.soTimeout = 5000 - val reader = BufferedReader(InputStreamReader(connectionSocket?.getInputStream())) - val outputStream = connectionSocket?.getOutputStream() - val writer = BufferedWriter(OutputStreamWriter(outputStream)) - this@RtspClient.reader = reader - this@RtspClient.writer = writer - writer.write(commandsManager.createOptions()) - writer.flush() - commandsManager.getResponse(reader, Method.OPTIONS) - writer.write(commandsManager.createAnnounce()) - writer.flush() + val socket = TcpStreamSocket(host, port, tlsEnabled, certificates) + this@RtspClient.socket = socket + socket.connect() + socket.write(commandsManager.createOptions()) + socket.flush() + commandsManager.getResponse(socket, Method.OPTIONS) + socket.write(commandsManager.createAnnounce()) + socket.flush() //check if you need credential for stream, if you need try connect with credential - val announceResponse = commandsManager.getResponse(reader, Method.ANNOUNCE) + val announceResponse = commandsManager.getResponse(socket, Method.ANNOUNCE) when (announceResponse.status) { 403 -> { onMainThread { @@ -282,9 +266,9 @@ class RtspClient(private val connectChecker: ConnectChecker) { } return@launch } else { - writer.write(commandsManager.createAnnounceWithAuth(announceResponse.text)) - writer.flush() - when (commandsManager.getResponse(reader, Method.ANNOUNCE).status) { + socket.write(commandsManager.createAnnounceWithAuth(announceResponse.text)) + socket.flush() + when (commandsManager.getResponse(socket, Method.ANNOUNCE).status) { 401 -> { onMainThread { connectChecker.onAuthError() @@ -316,9 +300,9 @@ class RtspClient(private val connectChecker: ConnectChecker) { } } if (!commandsManager.videoDisabled) { - writer.write(commandsManager.createSetup(RtpConstants.trackVideo)) - writer.flush() - val setupVideoStatus = commandsManager.getResponse(reader, Method.SETUP).status + socket.write(commandsManager.createSetup(RtpConstants.trackVideo)) + socket.flush() + val setupVideoStatus = commandsManager.getResponse(socket, Method.SETUP).status if (setupVideoStatus != 200) { onMainThread { connectChecker.onConnectionFailed("Error configure stream, setup video $setupVideoStatus") @@ -327,9 +311,9 @@ class RtspClient(private val connectChecker: ConnectChecker) { } } if (!commandsManager.audioDisabled) { - writer.write(commandsManager.createSetup(RtpConstants.trackAudio)) - writer.flush() - val setupAudioStatus = commandsManager.getResponse(reader, Method.SETUP).status + socket.write(commandsManager.createSetup(RtpConstants.trackAudio)) + socket.flush() + val setupAudioStatus = commandsManager.getResponse(socket, Method.SETUP).status if (setupAudioStatus != 200) { onMainThread { connectChecker.onConnectionFailed("Error configure stream, setup audio $setupAudioStatus") @@ -337,26 +321,16 @@ class RtspClient(private val connectChecker: ConnectChecker) { return@launch } } - writer.write(commandsManager.createRecord()) - writer.flush() - val recordStatus = commandsManager.getResponse(reader, Method.RECORD).status + socket.write(commandsManager.createRecord()) + socket.flush() + val recordStatus = commandsManager.getResponse(socket, Method.RECORD).status if (recordStatus != 200) { onMainThread { connectChecker.onConnectionFailed("Error configure stream, record $recordStatus") } return@launch } - outputStream?.let { out -> - rtspSender.setDataStream(out, host) - } - val videoPorts = commandsManager.videoServerPorts - val audioPorts = commandsManager.audioServerPorts - if (!commandsManager.videoDisabled) { - rtspSender.setVideoPorts(videoPorts[0], videoPorts[1]) - } - if (!commandsManager.audioDisabled) { - rtspSender.setAudioPorts(audioPorts[0], audioPorts[1]) - } + rtspSender.setSocket(socket) rtspSender.start() reTries = numRetry onMainThread { @@ -367,7 +341,7 @@ class RtspClient(private val connectChecker: ConnectChecker) { if (error != null) { Log.e(TAG, "connection error", error) onMainThread { - connectChecker.onConnectionFailed("Error configure stream, ${error.message}") + connectChecker.onConnectionFailed("Error configure stream, ${error.validMessage()}") } return@launch } @@ -381,9 +355,9 @@ class RtspClient(private val connectChecker: ConnectChecker) { val error = runCatching { if (isAlive()) { delay(2000) - reader?.let { r -> - if (r.ready()) { - val command = commandsManager.getResponse(r) + socket?.let { socket -> + if (socket.isConnected()) { + val command = commandsManager.getResponse(socket) //Do something depend of command if required } } @@ -394,7 +368,7 @@ class RtspClient(private val connectChecker: ConnectChecker) { scope.cancel() } }.exceptionOrNull() - if (error != null && error !is SocketTimeoutException) { + if (error != null && ConnectionFailed.parse(error.validMessage()) != ConnectionFailed.TIMEOUT) { scope.cancel() } } @@ -405,9 +379,9 @@ class RtspClient(private val connectChecker: ConnectChecker) { Your firewall could block it. */ private fun isAlive(): Boolean { - val connected = connectionSocket?.isConnected ?: false + val connected = socket?.isConnected() ?: false if (!checkServerAlive) return connected - val reachable = connectionSocket?.inetAddress?.isReachable(5000) ?: false + val reachable = socket?.isReachable() ?: false return if (connected && !reachable) false else connected } @@ -421,15 +395,11 @@ class RtspClient(private val connectChecker: ConnectChecker) { if (isStreaming) rtspSender.stop() val error = runCatching { withTimeoutOrNull(100) { - writer?.write(commandsManager.createTeardown()) - writer?.flush() + socket?.write(commandsManager.createTeardown()) + socket?.flush() } - connectionSocket?.close() - reader?.close() - reader = null - writer?.close() - writer = null - connectionSocket = null + socket?.close() + socket = null Log.i(TAG, "write teardown success") }.exceptionOrNull() if (error != null) { diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtspSender.kt b/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtspSender.kt index 90f7876c8..600c01c69 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtspSender.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtsp/RtspSender.kt @@ -23,7 +23,9 @@ import com.pedro.common.BitrateManager import com.pedro.common.ConnectChecker import com.pedro.common.VideoCodec import com.pedro.common.onMainThread +import com.pedro.common.socket.TcpStreamSocket import com.pedro.common.trySend +import com.pedro.common.validMessage import com.pedro.rtsp.rtcp.BaseSenderReport import com.pedro.rtsp.rtp.packets.* import com.pedro.rtsp.rtp.sockets.BaseRtpSocket @@ -40,7 +42,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.runInterruptible import java.io.IOException -import java.io.OutputStream import java.nio.ByteBuffer import java.util.* import java.util.concurrent.* @@ -81,9 +82,13 @@ class RtspSender( } @Throws(IOException::class) - fun setSocketsInfo(protocol: Protocol, videoSourcePorts: IntArray, audioSourcePorts: IntArray) { - rtpSocket = BaseRtpSocket.getInstance(protocol, videoSourcePorts[0], audioSourcePorts[0]) - baseSenderReport = BaseSenderReport.getInstance(protocol, videoSourcePorts[1], audioSourcePorts[1]) + fun setSocketsInfo( + protocol: Protocol, host: String, + videoSourcePorts: IntArray, audioSourcePorts: IntArray, + videoServerPorts: IntArray, audioServerPorts: IntArray, + ) { + rtpSocket = BaseRtpSocket.getInstance(protocol, host, videoSourcePorts[0], audioSourcePorts[0], videoServerPorts[0], audioServerPorts[0]) + baseSenderReport = BaseSenderReport.getInstance(protocol, host, videoSourcePorts[1], audioSourcePorts[1], videoServerPorts[1], audioServerPorts[1]) } fun setVideoInfo(sps: ByteArray, pps: ByteArray?, vps: ByteArray?) { @@ -109,17 +114,9 @@ class RtspSender( } @Throws(IOException::class) - fun setDataStream(outputStream: OutputStream, host: String) { - rtpSocket?.setDataStream(outputStream, host) - baseSenderReport?.setDataStream(outputStream, host) - } - - fun setVideoPorts(rtpPort: Int, rtcpPort: Int) { - videoPacket?.setPorts(rtpPort, rtcpPort) - } - - fun setAudioPorts(rtpPort: Int, rtcpPort: Int) { - audioPacket?.setPorts(rtpPort, rtcpPort) + suspend fun setSocket(socket: TcpStreamSocket) { + rtpSocket?.setSocket(socket) + baseSenderReport?.setSocket(socket) } fun sendVideoFrame(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) { @@ -199,7 +196,7 @@ class RtspSender( }.exceptionOrNull() if (error != null) { onMainThread { - connectChecker.onConnectionFailed("Error send packet, ${error.message}") + connectChecker.onConnectionFailed("Error send packet, ${error.validMessage()}") } Log.e(TAG, "send error: ", error) return@launch diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtsp/commands/CommandsManager.kt b/rtsp/src/main/java/com/pedro/rtsp/rtsp/commands/CommandsManager.kt index f56bf6a3a..ab77ea259 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtsp/commands/CommandsManager.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtsp/commands/CommandsManager.kt @@ -21,6 +21,7 @@ import com.pedro.common.AudioCodec import com.pedro.common.TimeUtils import com.pedro.common.VideoCodec import com.pedro.common.getMd5Hash +import com.pedro.common.socket.TcpStreamSocket import com.pedro.rtsp.rtsp.Protocol import com.pedro.rtsp.rtsp.commands.SdpBody.createAV1Body import com.pedro.rtsp.rtsp.commands.SdpBody.createAacBody @@ -31,7 +32,6 @@ import com.pedro.rtsp.rtsp.commands.SdpBody.createOpusBody import com.pedro.rtsp.utils.RtpConstants import com.pedro.rtsp.utils.encodeToString import com.pedro.rtsp.utils.getData -import java.io.BufferedReader import java.io.IOException import java.nio.ByteBuffer import java.util.regex.Pattern @@ -252,10 +252,10 @@ open class CommandsManager { } @Throws(IOException::class) - fun getResponse(reader: BufferedReader, method: Method = Method.UNKNOWN): Command { + suspend fun getResponse(socket: TcpStreamSocket, method: Method = Method.UNKNOWN): Command { var response = "" var line: String? - while (reader.readLine().also { line = it } != null) { + while (socket.readLine().also { line = it } != null) { response += "${line ?: ""}\n" //end of response if ((line?.length ?: 0) < 3) break diff --git a/rtsp/src/test/java/com/pedro/rtsp/rtcp/RtcpReportTest.kt b/rtsp/src/test/java/com/pedro/rtsp/rtcp/RtcpReportTest.kt index 7caafbf6a..ef7cef06a 100644 --- a/rtsp/src/test/java/com/pedro/rtsp/rtcp/RtcpReportTest.kt +++ b/rtsp/src/test/java/com/pedro/rtsp/rtcp/RtcpReportTest.kt @@ -17,6 +17,8 @@ package com.pedro.rtsp.rtcp import com.pedro.common.TimeUtils +import com.pedro.common.socket.TcpStreamSocket +import com.pedro.common.socket.UdpStreamSocket import com.pedro.rtsp.Utils import com.pedro.rtsp.rtsp.Protocol import com.pedro.rtsp.rtsp.RtpFrame @@ -45,9 +47,10 @@ import java.net.MulticastSocket class RtcpReportTest { @Mock - private lateinit var multicastSocketMocked: MulticastSocket + private lateinit var udpSocket: UdpStreamSocket @Mock - private lateinit var outputMocked: OutputStream + private lateinit var tcpSocket: TcpStreamSocket + private val timeUtilsMocked = Mockito.mockStatic(TimeUtils::class.java) private var fakeTime = 7502849023L @@ -64,11 +67,11 @@ class RtcpReportTest { @Test fun `GIVEN multiple video or audio rtp frames WHEN update rtcp tcp send THEN send only 1 of video and 1 of audio each 3 seconds`() = runTest { Utils.useStatics(listOf(timeUtilsMocked)) { - val senderReportTcp = BaseSenderReport.getInstance(Protocol.TCP, 0, 1) - senderReportTcp.setDataStream(outputMocked, "127.0.0.1") + val senderReportTcp = BaseSenderReport.getInstance(Protocol.TCP, "127.0.0.1", 0, 1, 2, 3) + senderReportTcp.setSocket(tcpSocket) senderReportTcp.setSSRC(0, 1) - val fakeFrameVideo = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, 0, 0, RtpConstants.trackVideo) - val fakeFrameAudio = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, 0, 0, RtpConstants.trackAudio) + val fakeFrameVideo = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, RtpConstants.trackVideo) + val fakeFrameAudio = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, RtpConstants.trackAudio) (0..10).forEach { value -> val frame = if (value % 2 == 0) fakeFrameVideo else fakeFrameAudio @@ -76,7 +79,7 @@ class RtcpReportTest { } val resultValue = argumentCaptor() withContext(Dispatchers.IO) { - verify(outputMocked, times((2))).write(resultValue.capture()) + verify(tcpSocket, times((2))).write(resultValue.capture()) } fakeTime += 3_000 //wait until next interval (0..10).forEach { value -> @@ -84,7 +87,7 @@ class RtcpReportTest { senderReportTcp.update(frame) } withContext(Dispatchers.IO) { - verify(outputMocked, times((4))).write(resultValue.capture()) + verify(tcpSocket, times((4))).write(resultValue.capture()) } } } @@ -92,18 +95,18 @@ class RtcpReportTest { @Test fun `GIVEN multiple video or audio rtp frames WHEN update rtcp udp send THEN send only 1 of video and 1 of audio each 3 seconds`() = runTest { Utils.useStatics(listOf(timeUtilsMocked)) { - val senderReportUdp = SenderReportUdp(11111, 11112, multicastSocketMocked, multicastSocketMocked) - senderReportUdp.setDataStream(outputMocked, "127.0.0.1") + val senderReportUdp = SenderReportUdp(udpSocket, udpSocket) + senderReportUdp.setSocket(tcpSocket) senderReportUdp.setSSRC(0, 1) - val fakeFrameVideo = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, 0, 0, RtpConstants.trackVideo) - val fakeFrameAudio = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, 0, 0, RtpConstants.trackAudio) + val fakeFrameVideo = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, RtpConstants.trackVideo) + val fakeFrameAudio = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, RtpConstants.trackAudio) (0..10).forEach { value -> val frame = if (value % 2 == 0) fakeFrameVideo else fakeFrameAudio senderReportUdp.update(frame) } - val resultValue = argumentCaptor() + val resultValue = argumentCaptor() withContext(Dispatchers.IO) { - verify(multicastSocketMocked, times((2))).send(resultValue.capture()) + verify(udpSocket, times((2))).writePacket(resultValue.capture()) } fakeTime += 3_000 //wait until next interval (0..10).forEach { value -> @@ -111,7 +114,7 @@ class RtcpReportTest { senderReportUdp.update(frame) } withContext(Dispatchers.IO) { - verify(multicastSocketMocked, times((4))).send(resultValue.capture()) + verify(udpSocket, times((4))).writePacket(resultValue.capture()) } } } diff --git a/rtsp/src/test/java/com/pedro/rtsp/rtp/AacPacketTest.kt b/rtsp/src/test/java/com/pedro/rtsp/rtp/AacPacketTest.kt index 9a7c3f4c3..28dacf4aa 100644 --- a/rtsp/src/test/java/com/pedro/rtsp/rtp/AacPacketTest.kt +++ b/rtsp/src/test/java/com/pedro/rtsp/rtp/AacPacketTest.kt @@ -40,7 +40,6 @@ class AacPacketTest { info.size = fakeAac.size info.flags = 1 val aacPacket = AacPacket(44100) - aacPacket.setPorts(1, 2) aacPacket.setSSRC(123456789) val frames = mutableListOf() aacPacket.createAndSendPacket(ByteBuffer.wrap(fakeAac), info) { @@ -50,7 +49,7 @@ class AacPacketTest { val expectedRtp = byteArrayOf(-128, -31, 0, 1, 0, 83, 19, 92, 7, 91, -51, 21, 0, 16, 9, 96).plus(fakeAac) val expectedTimeStamp = 5444444L val expectedSize = RtpConstants.RTP_HEADER_LENGTH + info.size + 4 - val packetResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, 1, 2, RtpConstants.trackAudio) + val packetResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, RtpConstants.trackAudio) assertEquals(1, frames.size) assertEquals(packetResult, frames[0]) } diff --git a/rtsp/src/test/java/com/pedro/rtsp/rtp/Av1PacketTest.kt b/rtsp/src/test/java/com/pedro/rtsp/rtp/Av1PacketTest.kt index 273aff808..29e6b1cc3 100644 --- a/rtsp/src/test/java/com/pedro/rtsp/rtp/Av1PacketTest.kt +++ b/rtsp/src/test/java/com/pedro/rtsp/rtp/Av1PacketTest.kt @@ -45,7 +45,6 @@ class Av1PacketTest { val frames = mutableListOf() val av1Packet = Av1Packet() - av1Packet.setPorts(1, 2) av1Packet.setSSRC(123456789) av1Packet.createAndSendPacket(ByteBuffer.wrap(av1data), info) { rtpFrames -> frames.addAll(rtpFrames) @@ -54,7 +53,7 @@ class Av1PacketTest { val expectedRtp = byteArrayOf(-128, -32, 0, 1, 0, -87, -118, -57, 7, 91, -51, 21, 24).plus(av1data) val expectedTimeStamp = 11111111L val expectedSize = RtpConstants.RTP_HEADER_LENGTH + info.size + 1 - val packetResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, 1, 2, RtpConstants.trackVideo) + val packetResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, RtpConstants.trackVideo) assertEquals(1, frames.size) assertEquals(packetResult, frames[0]) } diff --git a/rtsp/src/test/java/com/pedro/rtsp/rtp/G711PacketTest.kt b/rtsp/src/test/java/com/pedro/rtsp/rtp/G711PacketTest.kt index 6e5c89338..0262cc074 100644 --- a/rtsp/src/test/java/com/pedro/rtsp/rtp/G711PacketTest.kt +++ b/rtsp/src/test/java/com/pedro/rtsp/rtp/G711PacketTest.kt @@ -41,7 +41,6 @@ class G711PacketTest { info.size = fakeG711.size info.flags = 1 val g711Packet = G711Packet(8000) - g711Packet.setPorts(1, 2) g711Packet.setSSRC(123456789) val frames = mutableListOf() g711Packet.createAndSendPacket(ByteBuffer.wrap(fakeG711), info) { @@ -51,7 +50,7 @@ class G711PacketTest { val expectedRtp = byteArrayOf(-128, -120, 0, 1, 0, 15, 18, 6, 7, 91, -51, 21).plus(fakeG711) val expectedTimeStamp = 987654L val expectedSize = RtpConstants.RTP_HEADER_LENGTH + info.size - val packetResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, 1, 2, RtpConstants.trackAudio) + val packetResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, RtpConstants.trackAudio) assertEquals(1, frames.size) assertEquals(packetResult, frames[0]) } diff --git a/rtsp/src/test/java/com/pedro/rtsp/rtp/H264PacketTest.kt b/rtsp/src/test/java/com/pedro/rtsp/rtp/H264PacketTest.kt index e1c7bfe44..48fbea38a 100644 --- a/rtsp/src/test/java/com/pedro/rtsp/rtp/H264PacketTest.kt +++ b/rtsp/src/test/java/com/pedro/rtsp/rtp/H264PacketTest.kt @@ -46,7 +46,6 @@ class H264PacketTest { val fakeSps = byteArrayOf(0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04) val fakePps = byteArrayOf(0x00, 0x00, 0x00, 0x01, 0x0A, 0x0B, 0x0C) val h264Packet = H264Packet(fakeSps, fakePps) - h264Packet.setPorts(1, 2) h264Packet.setSSRC(123456789) val frames = mutableListOf() h264Packet.createAndSendPacket(ByteBuffer.wrap(fakeH264), info) { @@ -57,8 +56,8 @@ class H264PacketTest { val expectedStapA = byteArrayOf(-128, -32, 0, 1, 0, -87, -118, -57, 7, 91, -51, 21, 24, 0, 7, 0, 0, 0, 1, 2, 3, 4, 0, 7, 0, 0, 0, 1, 10, 11, 12) val expectedTimeStamp = 11111111L val expectedSize = RtpConstants.RTP_HEADER_LENGTH + 1 + info.size - header.size - val expectedStapAResult = RtpFrame(expectedStapA, expectedTimeStamp, fakePps.size + fakePps.size + 5 + RtpConstants.RTP_HEADER_LENGTH, 1, 2, RtpConstants.trackVideo) - val expectedPacketResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, 1, 2, RtpConstants.trackVideo) + val expectedStapAResult = RtpFrame(expectedStapA, expectedTimeStamp, fakePps.size + fakePps.size + 5 + RtpConstants.RTP_HEADER_LENGTH, RtpConstants.trackVideo) + val expectedPacketResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, RtpConstants.trackVideo) assertNotEquals(0, frames.size) assertTrue(frames.size == 2) @@ -81,7 +80,6 @@ class H264PacketTest { val fakeSps = byteArrayOf(0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04) val fakePps = byteArrayOf(0x00, 0x00, 0x00, 0x01, 0x0A, 0x0B, 0x0C) val h264Packet = H264Packet(fakeSps, fakePps) - h264Packet.setPorts(1, 2) h264Packet.setSSRC(123456789) val frames = mutableListOf() h264Packet.createAndSendPacket(ByteBuffer.wrap(fakeH264), info) { @@ -98,10 +96,10 @@ class H264PacketTest { val expectedTimeStamp = 11111111L val expectedSize = chunk1.size + RtpConstants.RTP_HEADER_LENGTH + 2 val expectedSize2 = chunk2.size + RtpConstants.RTP_HEADER_LENGTH + 2 - val expectedStapAResult = RtpFrame(expectedStapA, expectedTimeStamp, fakePps.size + fakePps.size + 5 + RtpConstants.RTP_HEADER_LENGTH, 1, 2, RtpConstants.trackVideo) + val expectedStapAResult = RtpFrame(expectedStapA, expectedTimeStamp, fakePps.size + fakePps.size + 5 + RtpConstants.RTP_HEADER_LENGTH, RtpConstants.trackVideo) - val expectedPacketResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, 1, 2, RtpConstants.trackVideo) - val expectedPacketResult2 = RtpFrame(expectedRtp2, expectedTimeStamp, expectedSize2, 1, 2, RtpConstants.trackVideo) + val expectedPacketResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, RtpConstants.trackVideo) + val expectedPacketResult2 = RtpFrame(expectedRtp2, expectedTimeStamp, expectedSize2, RtpConstants.trackVideo) assertNotEquals(0, frames.size) assertTrue(frames.size == 3) diff --git a/rtsp/src/test/java/com/pedro/rtsp/rtp/H265PacketTest.kt b/rtsp/src/test/java/com/pedro/rtsp/rtp/H265PacketTest.kt index 585d69e73..42b996a6b 100644 --- a/rtsp/src/test/java/com/pedro/rtsp/rtp/H265PacketTest.kt +++ b/rtsp/src/test/java/com/pedro/rtsp/rtp/H265PacketTest.kt @@ -44,7 +44,6 @@ class H265PacketTest { info.flags = 1 val h265Packet = H265Packet() - h265Packet.setPorts(1, 2) h265Packet.setSSRC(123456789) val frames = mutableListOf() h265Packet.createAndSendPacket(ByteBuffer.wrap(fakeH265), info) { @@ -54,7 +53,7 @@ class H265PacketTest { val expectedRtp = byteArrayOf(-128, -32, 0, 1, 0, -87, -118, -57, 7, 91, -51, 21, 5, 0).plus(fakeH265.copyOfRange(header.size, fakeH265.size)) val expectedTimeStamp = 11111111L val expectedSize = RtpConstants.RTP_HEADER_LENGTH + 2 + info.size - header.size - val expectedPacketResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, 1, 2, RtpConstants.trackVideo) + val expectedPacketResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, RtpConstants.trackVideo) assertNotEquals(0, frames.size) assertTrue(frames.size == 1) @@ -74,7 +73,6 @@ class H265PacketTest { info.flags = 1 val h265Packet = H265Packet() - h265Packet.setPorts(1, 2) h265Packet.setSSRC(123456789) val frames = mutableListOf() h265Packet.createAndSendPacket(ByteBuffer.wrap(fakeH265), info) { @@ -91,8 +89,8 @@ class H265PacketTest { val expectedSize = chunk1.size + RtpConstants.RTP_HEADER_LENGTH + 3 val expectedSize2 = chunk2.size + RtpConstants.RTP_HEADER_LENGTH + 3 - val expectedPacketResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, 1, 2, RtpConstants.trackVideo) - val expectedPacketResult2 = RtpFrame(expectedRtp2, expectedTimeStamp, expectedSize2, 1, 2, RtpConstants.trackVideo) + val expectedPacketResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, RtpConstants.trackVideo) + val expectedPacketResult2 = RtpFrame(expectedRtp2, expectedTimeStamp, expectedSize2, RtpConstants.trackVideo) assertNotEquals(0, frames.size) assertTrue(frames.size == 2) diff --git a/rtsp/src/test/java/com/pedro/rtsp/rtp/OpusPacketTest.kt b/rtsp/src/test/java/com/pedro/rtsp/rtp/OpusPacketTest.kt index 9d8cc69fb..41806c6d5 100644 --- a/rtsp/src/test/java/com/pedro/rtsp/rtp/OpusPacketTest.kt +++ b/rtsp/src/test/java/com/pedro/rtsp/rtp/OpusPacketTest.kt @@ -40,7 +40,6 @@ class OpusPacketTest { info.size = fakeOpus.size info.flags = 1 val opusPacket = OpusPacket(8000) - opusPacket.setPorts(1, 2) opusPacket.setSSRC(123456789) val frames = mutableListOf() opusPacket.createAndSendPacket(ByteBuffer.wrap(fakeOpus), info) { @@ -50,7 +49,7 @@ class OpusPacketTest { val expectedRtp = byteArrayOf(-128, -31, 0, 1, 0, 15, 18, 6, 7, 91, -51, 21).plus(fakeOpus) val expectedTimeStamp = 987654L val expectedSize = RtpConstants.RTP_HEADER_LENGTH + info.size - val packetResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, 1, 2, RtpConstants.trackAudio) + val packetResult = RtpFrame(expectedRtp, expectedTimeStamp, expectedSize, RtpConstants.trackAudio) assertEquals(1, frames.size) assertEquals(packetResult, frames[0]) } diff --git a/rtsp/src/test/java/com/pedro/rtsp/rtp/RtpSocketTest.kt b/rtsp/src/test/java/com/pedro/rtsp/rtp/RtpStreamSocketTest.kt similarity index 74% rename from rtsp/src/test/java/com/pedro/rtsp/rtp/RtpSocketTest.kt rename to rtsp/src/test/java/com/pedro/rtsp/rtp/RtpStreamSocketTest.kt index f9d8b5055..e098427d4 100644 --- a/rtsp/src/test/java/com/pedro/rtsp/rtp/RtpSocketTest.kt +++ b/rtsp/src/test/java/com/pedro/rtsp/rtp/RtpStreamSocketTest.kt @@ -16,6 +16,8 @@ package com.pedro.rtsp.rtp +import com.pedro.common.socket.TcpStreamSocket +import com.pedro.common.socket.UdpStreamSocket import com.pedro.rtsp.rtp.sockets.BaseRtpSocket import com.pedro.rtsp.rtp.sockets.RtpSocketUdp import com.pedro.rtsp.rtsp.Protocol @@ -31,50 +33,47 @@ import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.times import org.mockito.kotlin.verify -import java.io.OutputStream -import java.net.DatagramPacket -import java.net.MulticastSocket /** * Created by pedro on 9/9/23. */ @RunWith(MockitoJUnitRunner::class) -class RtpSocketTest { +class RtpStreamSocketTest { @Mock - private lateinit var multicastSocketMocked: MulticastSocket + private lateinit var udpSocket: UdpStreamSocket @Mock - private lateinit var outputMocked: OutputStream + private lateinit var tcpSocket: TcpStreamSocket @Test fun `GIVEN multiple video or audio rtp frames WHEN update rtcp tcp send THEN send only 1 of video and 1 of audio each 3 seconds`() = runTest { - val senderReportTcp = BaseRtpSocket.getInstance(Protocol.TCP, 0, 1) - senderReportTcp.setDataStream(outputMocked, "127.0.0.1") - val fakeFrameVideo = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, 0, 0, RtpConstants.trackVideo) - val fakeFrameAudio = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, 0, 0, RtpConstants.trackAudio) + val senderReportTcp = BaseRtpSocket.getInstance(Protocol.TCP, "127.0.0.1", 0, 1, 2, 3) + senderReportTcp.setSocket(tcpSocket) + val fakeFrameVideo = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, RtpConstants.trackVideo) + val fakeFrameAudio = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, RtpConstants.trackAudio) (0 until 10).forEach { value -> val frame = if (value % 2 == 0) fakeFrameVideo else fakeFrameAudio senderReportTcp.sendFrame(frame) } val resultValue = argumentCaptor() withContext(Dispatchers.IO) { - verify(outputMocked, times((10))).write(resultValue.capture()) + verify(tcpSocket, times((10))).write(resultValue.capture()) } } @Test fun `GIVEN multiple video or audio rtp frames WHEN update rtcp udp send THEN send only 1 of video and 1 of audio each 3 seconds`() = runTest { - val senderReportUdp = RtpSocketUdp(11111, 11112, multicastSocketMocked, multicastSocketMocked) - senderReportUdp.setDataStream(outputMocked, "127.0.0.1") - val fakeFrameVideo = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, 0, 0, RtpConstants.trackVideo) - val fakeFrameAudio = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, 0, 0, RtpConstants.trackAudio) + val senderReportUdp = RtpSocketUdp(udpSocket, udpSocket) + senderReportUdp.setSocket(tcpSocket) + val fakeFrameVideo = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, RtpConstants.trackVideo) + val fakeFrameAudio = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, RtpConstants.trackAudio) (0 until 10).forEach { value -> val frame = if (value % 2 == 0) fakeFrameVideo else fakeFrameAudio senderReportUdp.sendFrame(frame) } - val resultValue = argumentCaptor() + val resultValue = argumentCaptor() withContext(Dispatchers.IO) { - verify(multicastSocketMocked, times((10))).send(resultValue.capture()) + verify(udpSocket, times((10))).writePacket(resultValue.capture()) } } } \ No newline at end of file diff --git a/srt/src/main/java/com/pedro/srt/mpeg2ts/packets/H26XPacket.kt b/srt/src/main/java/com/pedro/srt/mpeg2ts/packets/H26XPacket.kt index ddd639deb..14db51ce1 100644 --- a/srt/src/main/java/com/pedro/srt/mpeg2ts/packets/H26XPacket.kt +++ b/srt/src/main/java/com/pedro/srt/mpeg2ts/packets/H26XPacket.kt @@ -17,7 +17,6 @@ package com.pedro.srt.mpeg2ts.packets import android.media.MediaCodec -import android.os.Build import android.util.Log import com.pedro.common.isKeyframe import com.pedro.common.removeInfo diff --git a/srt/src/main/java/com/pedro/srt/srt/CommandsManager.kt b/srt/src/main/java/com/pedro/srt/srt/CommandsManager.kt index 710975163..28a5e310d 100644 --- a/srt/src/main/java/com/pedro/srt/srt/CommandsManager.kt +++ b/srt/src/main/java/com/pedro/srt/srt/CommandsManager.kt @@ -93,7 +93,7 @@ class CommandsManager { } @Throws(IOException::class) - fun readHandshake(socket: SrtSocket?): Handshake { + suspend fun readHandshake(socket: SrtSocket?): Handshake { val handshakeBuffer = socket?.readBuffer() ?: throw IOException("read buffer failed, socket disconnected") val handshake = SrtPacket.getSrtPacket(handshakeBuffer) if (handshake is Handshake) { diff --git a/srt/src/main/java/com/pedro/srt/srt/SrtClient.kt b/srt/src/main/java/com/pedro/srt/srt/SrtClient.kt index bd875e358..3c852162e 100644 --- a/srt/src/main/java/com/pedro/srt/srt/SrtClient.kt +++ b/srt/src/main/java/com/pedro/srt/srt/SrtClient.kt @@ -20,9 +20,11 @@ import android.media.MediaCodec import android.util.Log import com.pedro.common.AudioCodec import com.pedro.common.ConnectChecker +import com.pedro.common.ConnectionFailed import com.pedro.common.UrlParser import com.pedro.common.VideoCodec import com.pedro.common.onMainThread +import com.pedro.common.validMessage import com.pedro.srt.srt.packets.ControlPacket import com.pedro.srt.srt.packets.DataPacket import com.pedro.srt.srt.packets.SrtPacket @@ -51,7 +53,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import java.io.IOException -import java.net.SocketTimeoutException import java.net.URISyntaxException import java.nio.ByteBuffer @@ -241,7 +242,7 @@ class SrtClient(private val connectChecker: ConnectChecker) { if (error != null) { Log.e(TAG, "connection error", error) onMainThread { - connectChecker.onConnectionFailed("Error configure stream, ${error.message}") + connectChecker.onConnectionFailed("Error configure stream, ${error.validMessage()}") } return@launch } @@ -301,6 +302,7 @@ class SrtClient(private val connectChecker: ConnectChecker) { while (scope.isActive && isStreaming) { val error = runCatching { if (isAlive()) { + delay(2000) //ignore packet after connect if tunneled to avoid spam idle handleMessages() } else { @@ -310,7 +312,7 @@ class SrtClient(private val connectChecker: ConnectChecker) { scope.cancel() } }.exceptionOrNull() - if (error != null && error !is SocketTimeoutException) { + if (error != null && ConnectionFailed.parse(error.validMessage()) != ConnectionFailed.TIMEOUT) { scope.cancel() } } 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 8659ba4cb..023328ea9 100644 --- a/srt/src/main/java/com/pedro/srt/srt/SrtSender.kt +++ b/srt/src/main/java/com/pedro/srt/srt/SrtSender.kt @@ -23,6 +23,7 @@ import com.pedro.common.BitrateManager import com.pedro.common.ConnectChecker import com.pedro.common.onMainThread import com.pedro.common.trySend +import com.pedro.common.validMessage import com.pedro.srt.mpeg2ts.MpegTsPacket import com.pedro.srt.mpeg2ts.MpegTsPacketizer import com.pedro.srt.mpeg2ts.Pid @@ -171,7 +172,7 @@ class SrtSender( }.exceptionOrNull() if (error != null) { onMainThread { - connectChecker.onConnectionFailed("Error send packet, " + error.message) + connectChecker.onConnectionFailed("Error send packet, ${error.validMessage()}") } Log.e(TAG, "send error: ", error) return@launch diff --git a/srt/src/main/java/com/pedro/srt/utils/SrtSocket.kt b/srt/src/main/java/com/pedro/srt/utils/SrtSocket.kt index f08b76677..4c1ceaf21 100644 --- a/srt/src/main/java/com/pedro/srt/utils/SrtSocket.kt +++ b/srt/src/main/java/com/pedro/srt/utils/SrtSocket.kt @@ -16,58 +16,32 @@ package com.pedro.srt.utils +import com.pedro.common.socket.UdpStreamSocket import com.pedro.srt.srt.packets.SrtPacket -import java.net.DatagramPacket -import java.net.DatagramSocket -import java.net.InetAddress /** * Created by pedro on 22/8/23. */ -class SrtSocket(private val host: String, private val port: Int) { +class SrtSocket(host: String, port: Int) { - private val TAG = "SrtSocket" - private var socket: DatagramSocket? = null - private var packetSize = Constants.MTU - private val timeout = 5000 + private val socket = UdpStreamSocket(host, port, receiveSize = Constants.MTU) - fun connect() { - val address = InetAddress.getByName(host) - socket = DatagramSocket() - socket?.connect(address, port) - socket?.soTimeout = timeout + suspend fun connect() { + socket.connect() } - fun close() { - if (socket?.isClosed == false) { - socket?.disconnect() - socket?.close() - socket = null - } + suspend fun close() { + socket.close() } - fun isConnected(): Boolean { - return socket?.isConnected ?: false - } + fun isConnected() = socket.isConnected() - fun isReachable(): Boolean { - return socket?.inetAddress?.isReachable(5000) ?: false - } + fun isReachable() = socket.isReachable() - fun setPacketSize(size: Int) { - packetSize = size - } - - fun write(srtPacket: SrtPacket) { + suspend fun write(srtPacket: SrtPacket) { val buffer = srtPacket.getData() - val udpPacket = DatagramPacket(buffer, buffer.size) - socket?.send(udpPacket) + socket.writePacket(buffer) } - fun readBuffer(): ByteArray { - val buffer = ByteArray(packetSize) - val udpPacket = DatagramPacket(buffer, buffer.size) - socket?.receive(udpPacket) - return udpPacket.data.sliceArray(0 until udpPacket.length) - } + suspend fun readBuffer() = socket.readPacket() } \ 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 index e58d49705..5b52faa03 100644 --- a/udp/src/main/java/com/pedro/udp/UdpClient.kt +++ b/udp/src/main/java/com/pedro/udp/UdpClient.kt @@ -23,6 +23,7 @@ import com.pedro.common.ConnectChecker import com.pedro.common.UrlParser import com.pedro.common.VideoCodec import com.pedro.common.onMainThread +import com.pedro.common.validMessage import com.pedro.udp.utils.UdpSocket import com.pedro.udp.utils.UdpType import kotlinx.coroutines.CoroutineScope @@ -169,7 +170,7 @@ class UdpClient(private val connectChecker: ConnectChecker) { if (error != null) { Log.e(TAG, "connection error", error) onMainThread { - connectChecker.onConnectionFailed("Error configure stream, ${error.message}") + connectChecker.onConnectionFailed("Error configure stream, ${error.validMessage()}") } return@launch } diff --git a/udp/src/main/java/com/pedro/udp/UdpSender.kt b/udp/src/main/java/com/pedro/udp/UdpSender.kt index 635e649cc..b748b0bff 100644 --- a/udp/src/main/java/com/pedro/udp/UdpSender.kt +++ b/udp/src/main/java/com/pedro/udp/UdpSender.kt @@ -23,6 +23,7 @@ import com.pedro.common.BitrateManager import com.pedro.common.ConnectChecker import com.pedro.common.onMainThread import com.pedro.common.trySend +import com.pedro.common.validMessage import com.pedro.srt.mpeg2ts.MpegTsPacket import com.pedro.srt.mpeg2ts.MpegTsPacketizer import com.pedro.srt.mpeg2ts.Pid @@ -171,7 +172,7 @@ class UdpSender( }.exceptionOrNull() if (error != null) { onMainThread { - connectChecker.onConnectionFailed("Error send packet, " + error.message) + connectChecker.onConnectionFailed("Error send packet, ${error.validMessage()}") } Log.e(TAG, "send error: ", error) return@launch 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 7c282e46d..55245cbcf 100644 --- a/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt +++ b/udp/src/main/java/com/pedro/udp/utils/UdpSocket.kt @@ -16,64 +16,37 @@ package com.pedro.udp.utils +import com.pedro.common.socket.UdpStreamSocket import com.pedro.srt.mpeg2ts.MpegTsPacket import com.pedro.srt.mpeg2ts.MpegTsPacketizer -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) { +class UdpSocket(host: String, type: UdpType, port: Int) { - private var socket: DatagramSocket? = null - private var packetSize = MpegTsPacketizer.packetSize - private val timeout = 5000 + private val socket = UdpStreamSocket( + host, port, receiveSize = MpegTsPacketizer.packetSize, + broadcastMode = type == UdpType.BROADCAST + ) - fun connect() { - val address = InetAddress.getByName(host) - socket = when (type) { - UdpType.UNICAST -> DatagramSocket() - UdpType.MULTICAST -> MulticastSocket() - UdpType.BROADCAST -> DatagramSocket().apply { broadcast = true } - } - socket?.connect(address, port) - socket?.soTimeout = timeout + suspend fun connect() { + socket.connect() } - fun close() { - if (socket?.isClosed == false) { - socket?.disconnect() - socket?.close() - socket = null - } + suspend fun close() { + socket.close() } - fun isConnected(): Boolean { - return socket?.isConnected ?: false - } + suspend fun isConnected() = socket.isConnected() - fun isReachable(): Boolean { - return socket?.inetAddress?.isReachable(5000) ?: false - } + fun isReachable() = socket.isReachable() - fun setPacketSize(size: Int) { - packetSize = size - } - - fun write(mpegTsPacket: MpegTsPacket): Int { + suspend fun write(mpegTsPacket: MpegTsPacket): Int { val buffer = mpegTsPacket.buffer - val udpPacket = DatagramPacket(buffer, buffer.size) - socket?.send(udpPacket) + socket.writePacket(buffer) 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) - } + suspend fun readBuffer() = socket.readPacket() } \ No newline at end of file