What is the good and right approach for Streaming ...
# ktor
s
What is the good and right approach for Streaming the data (in Kotlin Multiplatform)? In the documentation I found this: https://github.com/ktorio/ktor-documentation/blob/main/codeSnippets/snippets/client-download-streaming/src/main/kotlin/com/example/Application.kt I don't want to download the whole file, but only stream as much as I need. Single calls to channel.readByte() are not performant, so there must be some buffering solution. What is the way if I want to use it like a InputStream?
This is my solution so far, but it's slow and most likely wrong:
Copy code
override suspend fun getPhotoMetadataBytes(
    uri: String
): ImageMetadata? = withContext(dispatcher) {

    runCatchingNetworkException("OneDrive metadata") {

        val url = "$MICROSOFT_API_URL/me/drive/items/$uri/content"

        httpClient.prepareGet(url).execute { httpResponse ->

            val channel: ByteReadChannel = httpResponse.body()

            val length = httpResponse.contentLength()

            if (length == null) {
                Log.debug("No content length for request $uri")
                return@execute null
            }

            return@execute Kim.readMetadata(
                byteReader = KtorByteReadChannelByteReader(channel),
                length = length
            )
        }
    }
}

class KtorByteReadChannelByteReader(
    private val channel: ByteReadChannel,
    private val bufferSize: Long = DEFAULT_BUFFER_SIZE
) : ByteReader {

    private var buffer: ByteArray = byteArrayOf()
    private var bufferOffset = 0
    private var bufferLimit = 0

    override fun readByte(): Byte? {

        if (bufferOffset >= bufferLimit) {

            if (channel.isClosedForRead)
                return null

            // Buffer is empty, read more data from the channel
            buffer = runBlocking {
                channel.readRemaining(limit = bufferSize).readBytes()
            }
            bufferLimit = buffer.size
            bufferOffset = 0
        }

        return buffer[bufferOffset++]
    }

    override fun readBytes(count: Int): ByteArray {

        val result = ByteArray(count)
        var remaining = count
        var offset = 0

        while (remaining > 0) {

            if (bufferOffset >= bufferLimit) {

                if (channel.isClosedForRead)
                    break

                // Buffer is empty, read more data from the channel
                buffer = runBlocking {
                    channel.readRemaining(limit = bufferSize).readBytes()
                }
                bufferLimit = buffer.size
                bufferOffset = 0
            }

            val bytesToCopy = minOf(remaining, bufferLimit - bufferOffset)

            buffer.copyInto(result, offset, bufferOffset, bufferOffset + bytesToCopy)

            offset += bytesToCopy
            bufferOffset += bytesToCopy
            remaining -= bytesToCopy
        }

        return result
    }

    override fun close() {
        runBlocking {
            channel.cancel()
        }
    }

    companion object {
        private const val DEFAULT_BUFFER_SIZE: Long = 32 * 1024
    }
}
This is much faster, but it loads the whole file, which is a problem for traffic and memory
Copy code
val url = "$MICROSOFT_API_URL/me/drive/items/$uri/content"

val input: ByteReadPacket = httpClient.get(url).body()

Kim.readMetadata(input)
Is my code wrong or is streaming with Ktor in general super slow? I just found https://youtrack.jetbrains.com/issue/KTOR-3086/Slow-performance-of-streaming-response-bodies-in-kotlin-native-iOS which indicates that there may be a bigger problem. Does anyone use this streaming API in production? đŸ¤”
đŸ‘€ 1
a
Have you tried reading chunks from the response channel in a while loop using the
ByteReadChannel.readFully
method and then using the resulted
ByteArray
or
ByteBuffer
?
s
That sample code for streaming a file does not really work for my use case unfortunately. I use https://github.com/Ashampoo/kim to download as much of the cloud hosted JPG/PNG file as needed for extracting the metadata. That can be the first 10 kb, but also the first 100 kb. It has a variable size. So I want to let the library read from the stream until it detects the end of the image metadata block. My code above works, but it's slow and looks wrong to me. I feel that there must be a better and faster way to read the first bytes from a channel with some kind of buffering. Requesting byte per byte is super-slow, which may be the reason that the sample also uses a buffer.
a
If you have control over the library why can't you add one more
readMetadata
method which accepts a
ByteReadChannel
?
It's like
Input
which you already accept but asynchronous.
s
That is what I'm trying to do. Support in the library would be using the
KtorByteReadChannelByteReader
above. My problem is, that I'm not sure if this is the right implementation to use
ByteReadChannel
in a performant way.
a
What is the
ByteReader
and why do you have to implement it?
s
At it’s core it’s just a simple interface that’s like Java InputStream. Kim needs a way to get bytes from the stream. interface ByteReader : Closeable { fun readByte(): Byte? fun readBytes(count: Int): ByteArray }