Skip to content

Commit 6e39694

Browse files
committed
Catch known bugs, fix more hanging scenarios
1 parent c2929f6 commit 6e39694

File tree

9 files changed

+151
-132
lines changed

9 files changed

+151
-132
lines changed

lib/src/androidTest/java/com/otaliastudios/transcoder/integration/IssuesTests.kt

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import com.otaliastudios.transcoder.source.ClipDataSource
1515
import com.otaliastudios.transcoder.source.FileDescriptorDataSource
1616
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
1717
import com.otaliastudios.transcoder.validator.WriteAlwaysValidator
18+
import org.junit.Assume
19+
import org.junit.AssumptionViolatedException
1820
import org.junit.Test
1921
import org.junit.runner.RunWith
2022
import java.io.File
@@ -28,20 +30,21 @@ class IssuesTests {
2830
val context = InstrumentationRegistry.getInstrumentation().context
2931

3032
fun output(
31-
name: String = System.currentTimeMillis().toString(),
32-
extension: String = "mp4"
33+
name: String = System.currentTimeMillis().toString(),
34+
extension: String = "mp4"
3335
) = File(context.cacheDir, "$name.$extension").also { it.parentFile!!.mkdirs() }
3436

3537
fun input(filename: String) = AssetFileDescriptorDataSource(
36-
context.assets.openFd("issue_$issue/$filename")
38+
context.assets.openFd("issue_$issue/$filename")
3739
)
3840

3941
fun transcode(
40-
output: File = output(),
41-
assertTranscoded: Boolean = true,
42-
assertDuration: Boolean = true,
43-
builder: TranscoderOptions.Builder.() -> Unit,
44-
): File {
42+
output: File = output(),
43+
assertTranscoded: Boolean = true,
44+
assertDuration: Boolean = true,
45+
builder: TranscoderOptions.Builder.() -> Unit,
46+
): File = runCatching {
47+
Logger.setLogLevel(Logger.LEVEL_VERBOSE)
4548
val transcoder = Transcoder.into(output.absolutePath)
4649
transcoder.apply(builder)
4750
transcoder.setListener(object : TranscoderListener {
@@ -64,15 +67,19 @@ class IssuesTests {
6467
retriever.release()
6568
}
6669
return output
70+
}.getOrElse {
71+
if (it.toString().contains("c2.android.avc.encoder was unable to create the input surface (1x1)")) {
72+
log.w("Hit known emulator bug. Skipping the test.")
73+
throw AssumptionViolatedException("Hit known emulator bug.")
74+
}
75+
throw it
6776
}
6877
}
6978

7079

71-
@Test(timeout = 5000)
80+
@Test(timeout = 8000)
7281
fun issue137() = with(Helper(137)) {
7382
transcode {
74-
// addDataSource(ClipDataSource(input("main.mp3"), 0L, 200_000L))
75-
7683
addDataSource(ClipDataSource(input("main.mp3"), 0L, 1000_000L))
7784
addDataSource(input("0.amr"))
7885
addDataSource(ClipDataSource(input("main.mp3"), 2000_000L, 3000_000L))
@@ -95,7 +102,7 @@ class IssuesTests {
95102
Unit
96103
}
97104

98-
@Test(timeout = 5000)
105+
@Test(timeout = 8000)
99106
fun issue184() = with(Helper(184)) {
100107
transcode {
101108
addDataSource(TrackType.VIDEO, input("transcode.3gp"))
@@ -104,7 +111,7 @@ class IssuesTests {
104111
Unit
105112
}
106113

107-
@Test(timeout = 5000)
114+
@Test(timeout = 8000)
108115
fun issue102() = with(Helper(102)) {
109116
transcode {
110117
addDataSource(input("sample.mp4"))

lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
package com.otaliastudios.transcoder.internal
22

33
import android.media.MediaCodec
4-
import android.media.MediaCodecInfo
54
import android.media.MediaCodecList
65
import android.media.MediaFormat
76
import android.opengl.EGL14
8-
import android.view.Surface
97
import com.otaliastudios.opengl.core.EglCore
10-
import com.otaliastudios.opengl.surface.EglOffscreenSurface
118
import com.otaliastudios.opengl.surface.EglWindowSurface
129
import com.otaliastudios.transcoder.common.TrackStatus
1310
import com.otaliastudios.transcoder.common.TrackType
1411
import com.otaliastudios.transcoder.internal.media.MediaFormatConstants
1512
import com.otaliastudios.transcoder.internal.utils.Logger
1613
import com.otaliastudios.transcoder.internal.utils.TrackMap
14+
import java.nio.ByteBuffer
15+
import kotlin.properties.Delegates.observable
1716

1817
/**
1918
* Encoders are shared between segments. This is not strictly needed but it is more efficient
@@ -28,8 +27,8 @@ internal class Codecs(
2827
private val current: TrackMap<Int>
2928
) {
3029

31-
internal class Surface(
32-
val context: EglCore,
30+
class Surface(
31+
private val context: EglCore,
3332
val window: EglWindowSurface,
3433
) {
3534
fun release() {
@@ -38,17 +37,50 @@ internal class Codecs(
3837
}
3938
}
4039

40+
class Codec(val codec: MediaCodec, val surface: Surface? = null, var log: Logger? = null) {
41+
var dequeuedInputs by observable(0) { _, _, _ -> log?.v(state) }
42+
var dequeuedOutputs by observable(0) { _, _, _ -> log?.v(state) }
43+
val state get(): String = "dequeuedInputs=$dequeuedInputs dequeuedOutputs=$dequeuedOutputs heldInputs=${heldInputs.size}"
44+
45+
private val heldInputs = ArrayDeque<Pair<ByteBuffer, Int>>()
46+
47+
fun getInputBuffer(): Pair<ByteBuffer, Int>? {
48+
if (heldInputs.isNotEmpty()) {
49+
return heldInputs.removeFirst().also { log?.v(state) }
50+
}
51+
val id = codec.dequeueInputBuffer(100)
52+
return if (id >= 0) {
53+
dequeuedInputs++
54+
val buf = checkNotNull(codec.getInputBuffer(id)) { "inputBuffer($id) should not be null." }
55+
buf to id
56+
} else {
57+
log?.i("buffer() failed with $id. $state")
58+
null
59+
}
60+
}
61+
62+
/**
63+
* When we're not ready to write into this buffer, it can be held for later.
64+
* Previously we were returning it to the codec with timestamp=0, flags=0, but especially
65+
* on older Android versions that can create subtle issues.
66+
* It's better to just keep the buffer here and reuse it on the next [getInputBuffer] call.
67+
*/
68+
fun holdInputBuffer(buffer: ByteBuffer, id: Int) {
69+
heldInputs.addLast(buffer to id)
70+
}
71+
}
72+
4173
private val log = Logger("Codecs")
4274

43-
val encoders = object : TrackMap<Pair<MediaCodec, Surface?>> {
75+
val encoders = object : TrackMap<Codec> {
4476

4577
override fun has(type: TrackType) = tracks.all[type] == TrackStatus.COMPRESSING
4678

4779
private val lazyAudio by lazy {
4880
val format = tracks.outputFormats.audio
4981
val codec = MediaCodec.createEncoderByType(format.getString(MediaFormat.KEY_MIME)!!)
5082
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
51-
codec to null
83+
Codec(codec, null)
5284
}
5385

5486
private val lazyVideo by lazy {
@@ -85,7 +117,8 @@ internal class Codecs(
85117
error("c2.android.avc.encoder was unable to create the input surface (1x1).")
86118
}
87119
}
88-
codec to Surface(eglContext, eglWindow)
120+
121+
Codec(codec, Surface(eglContext, eglWindow))
89122
}
90123

91124
override fun get(type: TrackType) = when (type) {
@@ -106,7 +139,7 @@ internal class Codecs(
106139

107140
fun release() {
108141
encoders.forEach {
109-
it.first.release()
142+
it.surface?.release()
110143
}
111144
}
112145
}

lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import com.otaliastudios.transcoder.internal.utils.TrackMap
99
import com.otaliastudios.transcoder.internal.utils.mutableTrackMapOf
1010

1111
internal class Segments(
12-
private val sources: DataSources,
13-
private val tracks: Tracks,
14-
private val factory: (TrackType, Int, TrackStatus, MediaFormat) -> Pipeline
12+
private val sources: DataSources,
13+
private val tracks: Tracks,
14+
private val factory: (TrackType, Int, TrackStatus, MediaFormat) -> Pipeline
1515
) {
1616

1717
private val log = Logger("Segments")

lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,19 @@ internal class AudioEngine(
4242
}
4343

4444
override fun enqueueEos(data: DecoderData) {
45-
log.i("enqueueEos()")
45+
log.i("enqueueEos (${chunks.size} in queue)")
4646
data.release(false)
4747
chunks.enqueueEos()
4848
}
4949

5050
override fun enqueue(data: DecoderData) {
5151
val stretch = (data as? DecoderTimerData)?.timeStretch ?: 1.0
52-
chunks.enqueue(data.buffer.asShortBuffer(), data.timeUs, stretch) {
53-
log.v("drain(): releasing input data (decoder's outputBuffer)")
54-
data.release(false)
55-
}
52+
chunks.enqueue(data.buffer.asShortBuffer(), data.timeUs, stretch) { data.release(false) }
5653
}
5754

5855
override fun drain(): State<EncoderData> {
5956
if (!readyToDrain) {
60-
log.i("drain(): not ready, waiting...")
57+
log.i("drain(): not ready, waiting... (${chunks.size} in queue)")
6158
return State.Retry(false)
6259
}
6360
if (chunks.isEmpty()) {
@@ -67,7 +64,7 @@ internal class AudioEngine(
6764
}
6865
val (outBytes, outId) = next.buffer() ?: return run {
6966
// dequeueInputBuffer failed
70-
log.i("drain(): no next buffer, waiting...")
67+
log.i("drain(): no next buffer, waiting... (${chunks.size} in queue)")
7168
State.Retry(true)
7269
}
7370
val outBuffer = outBytes.asShortBuffer()
@@ -104,16 +101,17 @@ internal class AudioEngine(
104101

105102
// Resample
106103
resampler.resample(
107-
remixBuffer, rawFormat.sampleRate,
108-
outBuffer, targetFormat.sampleRate,
109-
targetFormat.channels)
104+
remixBuffer, rawFormat.sampleRate,
105+
outBuffer, targetFormat.sampleRate,
106+
targetFormat.channels
107+
)
110108
outBuffer.flip()
111109

112110
// Adjust position and dispatch.
113111
outBytes.clear()
114112
outBytes.limit(outBuffer.limit() * BYTES_PER_SHORT)
115113
outBytes.position(outBuffer.position() * BYTES_PER_SHORT)
116-
log.v("drain(): passing buffer $outId to encoder...")
114+
log.v("drain(): passing buffer $outId to encoder... ${chunks.size} in queue")
117115
State.Ok(EncoderData(outBytes, outId, timeUs))
118116
}
119117
}

lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,24 @@ internal class ChunkQueue(private val log: Logger) {
2929
private val pool = ShortBufferPool()
3030

3131
fun isEmpty() = queue.isEmpty()
32+
val size get() = queue.size
3233

3334
fun enqueue(buffer: ShortBuffer, timeUs: Long, timeStretch: Double, release: () -> Unit) {
3435
if (buffer.hasRemaining()) {
35-
log.v("[ChunkQueue] adding chunk at ${timeUs}us (${queue.size} => ${queue.size + 1})")
36-
queue.addLast(Chunk(buffer, timeUs, timeStretch, release))
36+
if (queue.size >= 3) {
37+
val copy = pool.take(buffer)
38+
queue.addLast(Chunk(copy, timeUs, timeStretch, { pool.give(copy) }))
39+
release()
40+
} else {
41+
queue.addLast(Chunk(buffer, timeUs, timeStretch, release))
42+
}
3743
} else {
38-
log.w("[ChunkQueue] enqueued invalid buffer ($timeUs, ${buffer.capacity()})")
44+
log.w("enqueued invalid buffer ($timeUs, ${buffer.capacity()})")
3945
release()
4046
}
4147
}
4248

4349
fun enqueueEos() {
44-
log.i("[ChunkQueue] adding EOS chunk (${queue.size} => ${queue.size + 1})")
4550
queue.addLast(Chunk.Eos)
4651
}
4752

@@ -67,10 +72,10 @@ internal class ChunkQueue(private val log: Logger) {
6772
release = { pool.give(buffer) },
6873
buffer = buffer
6974
))
70-
log.v("[ChunkQueue] partially handled chunk at ${head.timeUs}us, ${head.buffer.remaining()} bytes left (${queue.size})")
75+
log.v("drain(): partially handled chunk at ${head.timeUs}us, ${head.buffer.remaining()} bytes left (${queue.size})")
7176
} else {
7277
// buffer consumed!
73-
log.v("[ChunkQueue] consumed chunk at ${head.timeUs}us (${queue.size + 1} => ${queue.size})")
78+
log.v("drain(): consumed chunk at ${head.timeUs}us (${queue.size + 1} => ${queue.size})")
7479
head.release()
7580
}
7681
return result
@@ -86,7 +91,7 @@ class ShortBufferPool {
8691
val index = pool.indexOfFirst { it.capacity() >= needed }
8792
val memory = when {
8893
index >= 0 -> pool.removeAt(index)
89-
else -> ByteBuffer.allocateDirect(needed.coerceAtLeast(1024))
94+
else -> ByteBuffer.allocateDirect((needed * Short.SIZE_BYTES).coerceAtLeast(1024))
9095
.order(original.order())
9196
.asShortBuffer()
9297
}

0 commit comments

Comments
 (0)