Skip to content

Commit 23ac7bc

Browse files
authored
Merge f2ea2f0 into 4670d89
2 parents 4670d89 + f2ea2f0 commit 23ac7bc

6 files changed

Lines changed: 46 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@
201201
- Use `System.nanoTime()` for cron check-in duration measurement to avoid incorrect durations from wall-clock adjustments ([#5611](https://github.com/getsentry/sentry-java/pull/5611))
202202
- Fix crash when `getHistoricalProcessStartReasons` is called from an isolated or wrong-userId process ([#5597](https://github.com/getsentry/sentry-java/pull/5597))
203203
- Release `MediaMuxer` when a replay segment has no encodable frames to avoid a resource leak ([#5583](https://github.com/getsentry/sentry-java/pull/5583))
204+
- Release `MediaMuxer` when the replay video encoder fails to start to avoid a resource leak ([#5607](https://github.com/getsentry/sentry-java/pull/5607))
204205

205206
### Dependencies
206207

sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,16 @@ public class ReplayCache(private val options: SentryOptions, private val replayI
162162
bitRate = bitRate,
163163
),
164164
)
165-
.also { it.start() }
165+
.apply {
166+
// the constructor already opened the MediaMuxer, so release it if start() fails,
167+
// otherwise the encoder is never assigned and its resources leak (CloseGuard warning)
168+
try {
169+
start()
170+
} catch (t: Throwable) {
171+
release()
172+
throw t
173+
}
174+
}
166175
}
167176

168177
val step = 1000 / frameRate.toLong()

sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleMp4FrameMuxer.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ internal class SimpleMp4FrameMuxer(path: String, fps: Float) : SimpleFrameMuxer
6767
}
6868

6969
override fun release() {
70-
// stop() throws if the muxer was never started (e.g. no frame was ever muxed), so we guard it
71-
// to ensure release() is always reached and the underlying resources are freed
72-
if (started) {
70+
// stop() throws unless the muxer was started AND at least one sample was written, so we guard it
71+
// to ensure muxer.release() is always reached and the underlying resources are freed
72+
if (started && videoFrames > 0) {
7373
muxer.stop()
7474
}
7575
muxer.release()

sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,14 @@ internal class SimpleVideoEncoder(
287287
onClose?.invoke()
288288
drainCodec(true)
289289
mediaCodec.stop()
290+
} catch (e: RuntimeException) {
291+
options.logger.log(DEBUG, "Failed to properly release video encoder", e)
292+
} finally {
293+
// always release the native resources, even if draining/stopping the codec above threw (e.g.
294+
// when the encoder failed to fully start), otherwise they leak (CloseGuard warning)
290295
mediaCodec.release()
291296
surface?.release()
292-
293297
frameMuxer.release()
294-
} catch (e: Throwable) {
295-
options.logger.log(DEBUG, "Failed to properly release video encoder", e)
296298
}
297299
}
298300
}

sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicReference
2828
import kotlin.test.BeforeTest
2929
import kotlin.test.Test
3030
import kotlin.test.assertEquals
31+
import kotlin.test.assertFailsWith
3132
import kotlin.test.assertFalse
3233
import kotlin.test.assertNull
3334
import kotlin.test.assertTrue
@@ -36,6 +37,7 @@ import org.junit.rules.TemporaryFolder
3637
import org.junit.runner.RunWith
3738
import org.robolectric.annotation.Config
3839
import org.robolectric.shadows.ShadowBitmapFactory
40+
import org.robolectric.shadows.ShadowCloseGuard
3941

4042
@RunWith(AndroidJUnit4::class)
4143
@Config(sdk = [26], shadows = [ReplayShadowMediaCodec::class])
@@ -56,6 +58,7 @@ class ReplayCacheTest {
5658
@BeforeTest
5759
fun `set up`() {
5860
ReplayShadowMediaCodec.framesToEncode = 5
61+
ReplayShadowMediaCodec.throwOnStart = false
5962
ShadowBitmapFactory.setAllowInvalidImageData(true)
6063
}
6164

@@ -93,6 +96,26 @@ class ReplayCacheTest {
9396
assertNull(video)
9497
}
9598

99+
@Test
100+
fun `releases the muxer when the encoder fails to start`() {
101+
ReplayShadowMediaCodec.throwOnStart = true
102+
val replayCache = fixture.getSut(tmpDir)
103+
104+
val bitmap = Bitmap.createBitmap(1, 1, ARGB_8888)
105+
replayCache.addFrame(bitmap, 1)
106+
107+
ShadowCloseGuard.reset()
108+
assertFailsWith<IllegalStateException> {
109+
replayCache.createVideoOf(5000L, 0, 0, 100, 200, 1, 20_000)
110+
}
111+
112+
val muxerLeaks =
113+
ShadowCloseGuard.getErrors().filter { error ->
114+
error.stackTrace.any { it.className.contains("MediaMuxer") }
115+
}
116+
assertTrue(muxerLeaks.isEmpty(), "MediaMuxer was not released: $muxerLeaks")
117+
}
118+
96119
@Test
97120
fun `deletes frames after creating a video`() {
98121
ReplayShadowMediaCodec.framesToEncode = 3

sentry-android-replay/src/test/java/io/sentry/android/replay/util/ReplayShadowMediaCodec.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ class ReplayShadowMediaCodec : ShadowMediaCodec() {
1515
companion object {
1616
var frameRate = 1
1717
var framesToEncode = 5
18+
var throwOnStart = false
1819
}
1920

2021
private val encoded = AtomicBoolean(false)
2122

2223
@Implementation
2324
fun start() {
25+
if (throwOnStart) {
26+
throw IllegalStateException("Simulated codec start failure")
27+
}
2428
super.native_start()
2529
}
2630

0 commit comments

Comments
 (0)