davidliu
Committed by GitHub

Fix memory leaks (#170)

* Dispose of EglBase

* Dispose of SurfaceTextureHelper when disposing track, and dispose local tracks during cleanup

* Unneeded Surface creation

* Fix tests
@@ -18,6 +18,7 @@ import javax.inject.Singleton @@ -18,6 +18,7 @@ import javax.inject.Singleton
18 JsonFormatModule::class, 18 JsonFormatModule::class,
19 OverridesModule::class, 19 OverridesModule::class,
20 AudioHandlerModule::class, 20 AudioHandlerModule::class,
  21 + MemoryModule::class,
21 ] 22 ]
22 ) 23 )
23 internal interface LiveKitComponent { 24 internal interface LiveKitComponent {
  1 +package io.livekit.android.dagger
  2 +
  3 +import dagger.Module
  4 +import dagger.Provides
  5 +import io.livekit.android.memory.CloseableManager
  6 +import javax.inject.Singleton
  7 +
  8 +@Module
  9 +object MemoryModule {
  10 +
  11 + @Singleton
  12 + @Provides
  13 + fun closeableManager() = CloseableManager()
  14 +}
@@ -6,6 +6,7 @@ import androidx.annotation.Nullable @@ -6,6 +6,7 @@ import androidx.annotation.Nullable
6 import dagger.Module 6 import dagger.Module
7 import dagger.Provides 7 import dagger.Provides
8 import io.livekit.android.LiveKit 8 import io.livekit.android.LiveKit
  9 +import io.livekit.android.memory.CloseableManager
9 import io.livekit.android.util.LKLog 10 import io.livekit.android.util.LKLog
10 import io.livekit.android.util.LoggingLevel 11 import io.livekit.android.util.LoggingLevel
11 import io.livekit.android.webrtc.SimulcastVideoEncoderFactoryWrapper 12 import io.livekit.android.webrtc.SimulcastVideoEncoderFactoryWrapper
@@ -108,8 +109,11 @@ object RTCModule { @@ -108,8 +109,11 @@ object RTCModule {
108 109
109 @Provides 110 @Provides
110 @Singleton 111 @Singleton
111 - fun eglBase(): EglBase {  
112 - return EglBase.create() 112 + fun eglBase(@Singleton memoryManager: CloseableManager): EglBase {
  113 + val eglBase = EglBase.create()
  114 + memoryManager.registerResource(eglBase) { eglBase.release() }
  115 +
  116 + return eglBase
113 } 117 }
114 118
115 @Provides 119 @Provides
  1 +package io.livekit.android.memory
  2 +
  3 +import java.io.Closeable
  4 +
  5 +/**
  6 + * @hide
  7 + */
  8 +class CloseableManager : Closeable {
  9 +
  10 + private var isClosed = false
  11 + private val resources = mutableMapOf<Any, Closeable>()
  12 +
  13 + @Synchronized
  14 + fun registerResource(key: Any, closer: Closeable) {
  15 + if (isClosed) {
  16 + closer.close()
  17 + return
  18 + } else {
  19 + resources[key] = closer
  20 + }
  21 + }
  22 +
  23 + @Synchronized
  24 + fun registerClosable(closable: Closeable) {
  25 + if (isClosed) {
  26 + closable.close()
  27 + return
  28 + } else {
  29 + resources[closable] = closable
  30 + }
  31 + }
  32 +
  33 + @Synchronized
  34 + fun unregisterResource(key: Any): Closeable? {
  35 + return resources.remove(key)
  36 + }
  37 +
  38 + @Synchronized
  39 + override fun close() {
  40 + isClosed = true
  41 + resources.values.forEach { it.close() }
  42 + resources.clear()
  43 + }
  44 +}
@@ -15,7 +15,6 @@ import android.graphics.Matrix @@ -15,7 +15,6 @@ import android.graphics.Matrix
15 import android.graphics.SurfaceTexture 15 import android.graphics.SurfaceTexture
16 import android.os.Looper 16 import android.os.Looper
17 import android.util.AttributeSet 17 import android.util.AttributeSet
18 -import android.view.Surface  
19 import android.view.SurfaceHolder 18 import android.view.SurfaceHolder
20 import android.view.TextureView 19 import android.view.TextureView
21 import android.view.View 20 import android.view.View
@@ -283,7 +282,7 @@ open class TextureViewRenderer : TextureView, SurfaceHolder.Callback, TextureVie @@ -283,7 +282,7 @@ open class TextureViewRenderer : TextureView, SurfaceHolder.Callback, TextureVie
283 // TextureView.SurfaceTextureListener implementation 282 // TextureView.SurfaceTextureListener implementation
284 override fun onSurfaceTextureAvailable(surface: SurfaceTexture, i: Int, i1: Int) { 283 override fun onSurfaceTextureAvailable(surface: SurfaceTexture, i: Int, i1: Int) {
285 ThreadUtils.checkIsOnMainThread() 284 ThreadUtils.checkIsOnMainThread()
286 - eglRenderer.createEglSurface(Surface(surfaceTexture)) 285 + eglRenderer.createEglSurface(surfaceTexture)
287 surfaceHeight = 0 286 surfaceHeight = 0
288 surfaceWidth = surfaceHeight 287 surfaceWidth = surfaceHeight
289 updateSurfaceSize() 288 updateSurfaceSize()
@@ -20,6 +20,7 @@ import io.livekit.android.events.BroadcastEventBus @@ -20,6 +20,7 @@ import io.livekit.android.events.BroadcastEventBus
20 import io.livekit.android.events.ParticipantEvent 20 import io.livekit.android.events.ParticipantEvent
21 import io.livekit.android.events.RoomEvent 21 import io.livekit.android.events.RoomEvent
22 import io.livekit.android.events.collect 22 import io.livekit.android.events.collect
  23 +import io.livekit.android.memory.CloseableManager
23 import io.livekit.android.renderer.TextureViewRenderer 24 import io.livekit.android.renderer.TextureViewRenderer
24 import io.livekit.android.room.participant.* 25 import io.livekit.android.room.participant.*
25 import io.livekit.android.room.track.* 26 import io.livekit.android.room.track.*
@@ -33,6 +34,7 @@ import livekit.LivekitModels @@ -33,6 +34,7 @@ import livekit.LivekitModels
33 import livekit.LivekitRtc 34 import livekit.LivekitRtc
34 import org.webrtc.* 35 import org.webrtc.*
35 import javax.inject.Named 36 import javax.inject.Named
  37 +import javax.inject.Singleton
36 38
37 class Room 39 class Room
38 @AssistedInject 40 @AssistedInject
@@ -47,6 +49,8 @@ constructor( @@ -47,6 +49,8 @@ constructor(
47 @Named(InjectionNames.DISPATCHER_IO) 49 @Named(InjectionNames.DISPATCHER_IO)
48 private val ioDispatcher: CoroutineDispatcher, 50 private val ioDispatcher: CoroutineDispatcher,
49 val audioHandler: AudioHandler, 51 val audioHandler: AudioHandler,
  52 + @Singleton
  53 + private val closeableManager: CloseableManager,
50 ) : RTCEngine.Listener, ParticipantListener { 54 ) : RTCEngine.Listener, ParticipantListener {
51 55
52 private lateinit var coroutineScope: CoroutineScope 56 private lateinit var coroutineScope: CoroutineScope
@@ -250,6 +254,16 @@ constructor( @@ -250,6 +254,16 @@ constructor(
250 } 254 }
251 255
252 /** 256 /**
  257 + * Release all resources held by this object.
  258 + *
  259 + * Once called, this room object must not be used to connect to a server and a new one
  260 + * must be created.
  261 + */
  262 + fun release() {
  263 + closeableManager.close()
  264 + }
  265 +
  266 + /**
253 * @suppress 267 * @suppress
254 */ 268 */
255 override fun onJoinResponse(response: LivekitRtc.JoinResponse) { 269 override fun onJoinResponse(response: LivekitRtc.JoinResponse) {
@@ -568,6 +568,7 @@ internal constructor( @@ -568,6 +568,7 @@ internal constructor(
568 if (track != null) { 568 if (track != null) {
569 track.stop() 569 track.stop()
570 unpublishTrack(track) 570 unpublishTrack(track)
  571 + track.dispose()
571 } 572 }
572 } 573 }
573 } 574 }
@@ -8,6 +8,7 @@ import androidx.core.content.ContextCompat @@ -8,6 +8,7 @@ import androidx.core.content.ContextCompat
8 import dagger.assisted.Assisted 8 import dagger.assisted.Assisted
9 import dagger.assisted.AssistedFactory 9 import dagger.assisted.AssistedFactory
10 import dagger.assisted.AssistedInject 10 import dagger.assisted.AssistedInject
  11 +import io.livekit.android.memory.CloseableManager
11 import io.livekit.android.room.DefaultsManager 12 import io.livekit.android.room.DefaultsManager
12 import io.livekit.android.room.track.video.Camera1CapturerWithSize 13 import io.livekit.android.room.track.video.Camera1CapturerWithSize
13 import io.livekit.android.room.track.video.Camera2CapturerWithSize 14 import io.livekit.android.room.track.video.Camera2CapturerWithSize
@@ -56,6 +57,8 @@ constructor( @@ -56,6 +57,8 @@ constructor(
56 private val sender: RtpSender? 57 private val sender: RtpSender?
57 get() = transceiver?.sender 58 get() = transceiver?.sender
58 59
  60 + private val closeableManager = CloseableManager()
  61 +
59 open fun startCapture() { 62 open fun startCapture() {
60 capturer.startCapture( 63 capturer.startCapture(
61 options.captureParams.width, 64 options.captureParams.width,
@@ -73,6 +76,12 @@ constructor( @@ -73,6 +76,12 @@ constructor(
73 super.stop() 76 super.stop()
74 } 77 }
75 78
  79 + override fun dispose() {
  80 + super.dispose()
  81 + capturer.dispose()
  82 + closeableManager.close()
  83 + }
  84 +
76 fun setDeviceId(deviceId: String) { 85 fun setDeviceId(deviceId: String) {
77 restartTrack(options.copy(deviceId = deviceId)) 86 restartTrack(options.copy(deviceId = deviceId))
78 } 87 }
@@ -132,14 +141,6 @@ constructor( @@ -132,14 +141,6 @@ constructor(
132 * Restart a track with new options. 141 * Restart a track with new options.
133 */ 142 */
134 fun restartTrack(options: LocalVideoTrackOptions = defaultsManager.videoTrackCaptureDefaults.copy()) { 143 fun restartTrack(options: LocalVideoTrackOptions = defaultsManager.videoTrackCaptureDefaults.copy()) {
135 - val newTrack = createTrack(  
136 - peerConnectionFactory,  
137 - context,  
138 - name,  
139 - options,  
140 - eglBase,  
141 - trackFactory  
142 - )  
143 144
144 val oldCapturer = capturer 145 val oldCapturer = capturer
145 val oldSource = source 146 val oldSource = source
@@ -152,6 +153,18 @@ constructor( @@ -152,6 +153,18 @@ constructor(
152 // sender owns rtcTrack, so it'll take care of disposing it. 153 // sender owns rtcTrack, so it'll take care of disposing it.
153 oldRtcTrack.setEnabled(false) 154 oldRtcTrack.setEnabled(false)
154 155
  156 + val oldCloseable = closeableManager.unregisterResource(oldRtcTrack)
  157 + oldCloseable?.close()
  158 +
  159 + val newTrack = createTrack(
  160 + peerConnectionFactory,
  161 + context,
  162 + name,
  163 + options,
  164 + eglBase,
  165 + trackFactory
  166 + )
  167 +
155 // migrate video sinks to the new track 168 // migrate video sinks to the new track
156 for (sink in sinks) { 169 for (sink in sinks) {
157 oldRtcTrack.removeSink(sink) 170 oldRtcTrack.removeSink(sink)
@@ -185,24 +198,28 @@ constructor( @@ -185,24 +198,28 @@ constructor(
185 name: String, 198 name: String,
186 capturer: VideoCapturer, 199 capturer: VideoCapturer,
187 options: LocalVideoTrackOptions = LocalVideoTrackOptions(), 200 options: LocalVideoTrackOptions = LocalVideoTrackOptions(),
  201 +
188 rootEglBase: EglBase, 202 rootEglBase: EglBase,
189 trackFactory: Factory 203 trackFactory: Factory
190 ): LocalVideoTrack { 204 ): LocalVideoTrack {
191 val source = peerConnectionFactory.createVideoSource(false) 205 val source = peerConnectionFactory.createVideoSource(false)
  206 + val surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext)
192 capturer.initialize( 207 capturer.initialize(
193 - SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext), 208 + surfaceTextureHelper,
194 context, 209 context,
195 source.capturerObserver 210 source.capturerObserver
196 ) 211 )
197 - val track = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source) 212 + val rtcTrack = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source)
198 213
199 - return trackFactory.create( 214 + val track = trackFactory.create(
200 capturer = capturer, 215 capturer = capturer,
201 source = source, 216 source = source,
202 options = options, 217 options = options,
203 name = name, 218 name = name,
204 - rtcTrack = track 219 + rtcTrack = rtcTrack
205 ) 220 )
  221 + track.closeableManager.registerResource(rtcTrack) { surfaceTextureHelper.dispose() }
  222 + return track
206 } 223 }
207 internal fun createTrack( 224 internal fun createTrack(
208 peerConnectionFactory: PeerConnectionFactory, 225 peerConnectionFactory: PeerConnectionFactory,
@@ -221,20 +238,25 @@ constructor( @@ -221,20 +238,25 @@ constructor(
221 238
222 val source = peerConnectionFactory.createVideoSource(options.isScreencast) 239 val source = peerConnectionFactory.createVideoSource(options.isScreencast)
223 val (capturer, newOptions) = createVideoCapturer(context, options) ?: TODO() 240 val (capturer, newOptions) = createVideoCapturer(context, options) ?: TODO()
  241 + val surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext)
224 capturer.initialize( 242 capturer.initialize(
225 - SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext), 243 + surfaceTextureHelper,
226 context, 244 context,
227 source.capturerObserver 245 source.capturerObserver
228 ) 246 )
229 - val track = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source) 247 + val rtcTrack = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source)
230 248
231 - return trackFactory.create( 249 + val track = trackFactory.create(
232 capturer = capturer, 250 capturer = capturer,
233 source = source, 251 source = source,
234 options = newOptions, 252 options = newOptions,
235 name = name, 253 name = name,
236 - rtcTrack = track 254 + rtcTrack = rtcTrack
237 ) 255 )
  256 +
  257 + track.closeableManager.registerResource(rtcTrack) { surfaceTextureHelper.dispose() }
  258 +
  259 + return track
238 } 260 }
239 261
240 private fun createCameraEnumerator(context: Context): CameraEnumerator { 262 private fun createCameraEnumerator(context: Context): CameraEnumerator {
@@ -5,6 +5,7 @@ import dagger.BindsInstance @@ -5,6 +5,7 @@ import dagger.BindsInstance
5 import dagger.Component 5 import dagger.Component
6 import io.livekit.android.dagger.JsonFormatModule 6 import io.livekit.android.dagger.JsonFormatModule
7 import io.livekit.android.dagger.LiveKitComponent 7 import io.livekit.android.dagger.LiveKitComponent
  8 +import io.livekit.android.dagger.MemoryModule
8 import io.livekit.android.mock.MockWebSocketFactory 9 import io.livekit.android.mock.MockWebSocketFactory
9 import io.livekit.android.room.RTCEngine 10 import io.livekit.android.room.RTCEngine
10 import javax.inject.Singleton 11 import javax.inject.Singleton
@@ -17,6 +18,7 @@ import javax.inject.Singleton @@ -17,6 +18,7 @@ import javax.inject.Singleton
17 TestWebModule::class, 18 TestWebModule::class,
18 TestAudioHandlerModule::class, 19 TestAudioHandlerModule::class,
19 JsonFormatModule::class, 20 JsonFormatModule::class,
  21 + MemoryModule::class,
20 ] 22 ]
21 ) 23 )
22 internal interface TestLiveKitComponent : LiveKitComponent { 24 internal interface TestLiveKitComponent : LiveKitComponent {
@@ -11,6 +11,7 @@ import io.livekit.android.events.EventCollector @@ -11,6 +11,7 @@ import io.livekit.android.events.EventCollector
11 import io.livekit.android.events.EventListenable 11 import io.livekit.android.events.EventListenable
12 import io.livekit.android.events.ParticipantEvent 12 import io.livekit.android.events.ParticipantEvent
13 import io.livekit.android.events.RoomEvent 13 import io.livekit.android.events.RoomEvent
  14 +import io.livekit.android.memory.CloseableManager
14 import io.livekit.android.mock.* 15 import io.livekit.android.mock.*
15 import io.livekit.android.room.participant.LocalParticipant 16 import io.livekit.android.room.participant.LocalParticipant
16 import kotlinx.coroutines.ExperimentalCoroutinesApi 17 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -74,6 +75,7 @@ class RoomTest { @@ -74,6 +75,7 @@ class RoomTest {
74 defaultDispatcher = coroutineRule.dispatcher, 75 defaultDispatcher = coroutineRule.dispatcher,
75 ioDispatcher = coroutineRule.dispatcher, 76 ioDispatcher = coroutineRule.dispatcher,
76 audioHandler = NoAudioHandler(), 77 audioHandler = NoAudioHandler(),
  78 + closeableManager = CloseableManager()
77 ) 79 )
78 } 80 }
79 81
@@ -215,6 +215,7 @@ class CallViewModel( @@ -215,6 +215,7 @@ class CallViewModel(
215 override fun onCleared() { 215 override fun onCleared() {
216 super.onCleared() 216 super.onCleared()
217 room.disconnect() 217 room.disconnect()
  218 + room.release()
218 } 219 }
219 220
220 fun setMicEnabled(enabled: Boolean) { 221 fun setMicEnabled(enabled: Boolean) {