diff --git a/core-util-jvm/src/main/java/org/signal/core/util/InputStreamExtensions.kt b/core-util-jvm/src/main/java/org/signal/core/util/InputStreamExtensions.kt index 5a6880c835..877cf7e7aa 100644 --- a/core-util-jvm/src/main/java/org/signal/core/util/InputStreamExtensions.kt +++ b/core-util-jvm/src/main/java/org/signal/core/util/InputStreamExtensions.kt @@ -5,9 +5,9 @@ package org.signal.core.util +import java.io.EOFException import java.io.IOException import java.io.InputStream -import kotlin.jvm.Throws /** * Reads a 32-bit variable-length integer from the stream. @@ -80,3 +80,26 @@ fun InputStream.readLength(): Long { return count } + +/** + * Backported from AOSP API 31 source code. + * + * @param count number of bytes to skip + */ +@Throws(IOException::class) +fun InputStream.skipNBytesCompat(count: Long) { + var n = count + while (n > 0) { + val ns = skip(n) + if (ns in 1..n) { + n -= ns + } else if (ns == 0L) { + if (read() == -1) { + throw EOFException() + } + n-- + } else { + throw IOException("Unable to skip exactly") + } + } +} diff --git a/video/lib/src/main/java/org/thoughtcrime/securesms/video/videoconverter/mediadatasource/InputStreamMediaDataSource.kt b/video/lib/src/main/java/org/thoughtcrime/securesms/video/videoconverter/mediadatasource/InputStreamMediaDataSource.kt index db62e28c1b..5905d22e24 100644 --- a/video/lib/src/main/java/org/thoughtcrime/securesms/video/videoconverter/mediadatasource/InputStreamMediaDataSource.kt +++ b/video/lib/src/main/java/org/thoughtcrime/securesms/video/videoconverter/mediadatasource/InputStreamMediaDataSource.kt @@ -7,6 +7,8 @@ package org.thoughtcrime.securesms.video.videoconverter.mediadatasource import android.media.MediaDataSource import androidx.annotation.RequiresApi +import org.signal.core.util.skipNBytesCompat +import java.io.EOFException import java.io.IOException import java.io.InputStream @@ -15,30 +17,50 @@ import java.io.InputStream */ @RequiresApi(23) abstract class InputStreamMediaDataSource : MediaDataSource() { + private var lastPositionRead = -1L + private var lastUsedInputStream: InputStream? = null + private val sink = ByteArray(2048) + @Throws(IOException::class) override fun readAt(position: Long, bytes: ByteArray?, offset: Int, length: Int): Int { - if (position >= size) { + if (position >= size || position < 0) { return -1 } - createInputStream(position).use { inputStream -> - var totalRead = 0 - while (totalRead < length) { - val read: Int = inputStream.read(bytes, offset + totalRead, length - totalRead) - if (read == -1) { - return if (totalRead == 0) { - -1 - } else { - totalRead - } - } - totalRead += read - } - return totalRead + val inputStream = if (lastPositionRead > position || lastUsedInputStream == null) { + lastUsedInputStream?.close() + lastPositionRead = position + createInputStream(position) + } else { + lastUsedInputStream!! } + + try { + inputStream.skipNBytesCompat(position - lastPositionRead) + } catch (e: EOFException) { + return -1 + } + + var totalRead = 0 + while (totalRead < length) { + val read: Int = inputStream.read(bytes, offset + totalRead, length - totalRead) + if (read == -1) { + return if (totalRead == 0) { + -1 + } else { + totalRead + } + } + totalRead += read + } + lastPositionRead = totalRead + position + lastUsedInputStream = inputStream + return totalRead } - abstract override fun close() + override fun close() { + lastUsedInputStream?.close() + } abstract override fun getSize(): Long diff --git a/video/lib/src/test/java/org/thoughtcrime/securesms/video/videoconverter/mediadatasource/InputStreamMediaDataSourceTest.kt b/video/lib/src/test/java/org/thoughtcrime/securesms/video/videoconverter/mediadatasource/InputStreamMediaDataSourceTest.kt new file mode 100644 index 0000000000..6eaf357ef3 --- /dev/null +++ b/video/lib/src/test/java/org/thoughtcrime/securesms/video/videoconverter/mediadatasource/InputStreamMediaDataSourceTest.kt @@ -0,0 +1,119 @@ +package org.thoughtcrime.securesms.video.videoconverter.mediadatasource + +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.signal.core.util.skipNBytesCompat +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.util.Arrays +import kotlin.random.Random + +@OptIn(ExperimentalStdlibApi::class) +class InputStreamMediaDataSourceTest { + companion object { + const val BUFFER_SIZE = 1024 + const val DATA_SIZE = 8192 + } + private lateinit var dataSource: TestInputStreamMediaDataSource + private val outputBuffer = ByteArray(BUFFER_SIZE) + + @Before + fun setUp() { + dataSource = TestInputStreamMediaDataSource(Random.Default.nextBytes(DATA_SIZE)) + Arrays.fill(outputBuffer, 0) + } + + /** + * Happy path test for reading from the start of the stream. + */ + @Test + fun testStartRead() { + val readLength = BUFFER_SIZE + dataSource.readAt(0, outputBuffer, 0, readLength) + assertArrayEquals(dataSource.getSliceOfData(0..n were requested + */ + @Test + fun testReadPastInputStreamSize() { + val readLength = 512 + val distanceFromEnd = readLength / 2 + val skipOffset = DATA_SIZE - distanceFromEnd + val readResult = dataSource.readAt(skipOffset.toLong(), outputBuffer, 0, readLength) + + assertEquals(distanceFromEnd, readResult) + assertArrayEquals(dataSource.getSliceOfData(skipOffset..n were requested + */ + @Test + fun testReadUpToEndAndThenKeepReading() { + val readLength = 512 + val distanceFromEnd = readLength / 2 + val skipOffset = DATA_SIZE - distanceFromEnd + + val readResultLastOfStream = dataSource.readAt(skipOffset.toLong(), outputBuffer, 0, readLength) + val readResultAtEndOfStream = dataSource.readAt((skipOffset + readResultLastOfStream).toLong(), outputBuffer, 0, readLength) + + assertEquals(-1, readResultAtEndOfStream) + assertArrayEquals(dataSource.getSliceOfData(skipOffset..