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
正在显示
11 个修改的文件
包含
124 行增加
和
20 行删除
| @@ -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) { |
| @@ -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) { |
-
请 注册 或 登录 后发表评论