Move ExoPlayer initialization to a Dagger module and optimize its dependencies (#4296)
Currently, ExoPlayer is initialized explicitly in `ViewMediaFragment` with all its dependencies, including many that are not useful for viewing Mastodon media attachments. This pull request moves most ExoPlayer initialization and configuration to a new Dagger module, and instead a `Provider<ExoPlayer>` factory is injected in the Fragment so it can create new instances when needed. The following ExoPlayer components will be configured: - **Renderers**: all of them (audio, video, metadata, subtitles) except for the `CameraMotionRenderer`. - **Extractors**: FLAC, Wav, Mp4, Ogg, Matroska/WebM and MP3 containers, to provide the same support as Firefox or Chrome browsers. Other container formats that are either image formats (already covered by Glide), not web-friendly or reserved for live streaming are skipped. - **MediaSource**: only progressive download (through OkHttp) is provided. Live streaming support using protocols like RTSP, MPEG/Dash or HLS is skipped, because Mastodon servers don't use these protocols to download attachments. The Mastodon documentation mentions the [supported media formats for attachments](https://docs.joinmastodon.org/user/posting/#media) and this covers them and even more. The docs also mentions that the video and audio files are transcoded to MP4 and MP3 upon upload but that was not the case in the past (for example WebM was used) and it could change again in the future. Specifying these components manually allows reducing the APK size by about 200 KB thanks to R8 shrinking. There are also a few extra code changes: - Remove the code specific to API < 24 since the min SDK of the app is now 24. - Add support for pausing a video when unplugging headphones. - Specify the audio attributes according to content type to help the Android audio mixer.
This commit is contained in:
parent
f09a5b00e0
commit
9901376d38
5 changed files with 179 additions and 74 deletions
|
@ -36,7 +36,8 @@ import javax.inject.Singleton
|
||||||
ServicesModule::class,
|
ServicesModule::class,
|
||||||
BroadcastReceiverModule::class,
|
BroadcastReceiverModule::class,
|
||||||
ViewModelModule::class,
|
ViewModelModule::class,
|
||||||
WorkerModule::class
|
WorkerModule::class,
|
||||||
|
PlayerModule::class
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
interface AppComponent {
|
interface AppComponent {
|
||||||
|
|
139
app/src/main/java/com/keylesspalace/tusky/di/PlayerModule.kt
Normal file
139
app/src/main/java/com/keylesspalace/tusky/di/PlayerModule.kt
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.C
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.datasource.DataSource
|
||||||
|
import androidx.media3.datasource.DefaultDataSource
|
||||||
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.exoplayer.RenderersFactory
|
||||||
|
import androidx.media3.exoplayer.audio.AudioSink
|
||||||
|
import androidx.media3.exoplayer.audio.DefaultAudioSink
|
||||||
|
import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer
|
||||||
|
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
|
||||||
|
import androidx.media3.exoplayer.metadata.MetadataRenderer
|
||||||
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
|
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||||
|
import androidx.media3.exoplayer.text.TextRenderer
|
||||||
|
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer
|
||||||
|
import androidx.media3.extractor.ExtractorsFactory
|
||||||
|
import androidx.media3.extractor.flac.FlacExtractor
|
||||||
|
import androidx.media3.extractor.mkv.MatroskaExtractor
|
||||||
|
import androidx.media3.extractor.mp3.Mp3Extractor
|
||||||
|
import androidx.media3.extractor.mp4.FragmentedMp4Extractor
|
||||||
|
import androidx.media3.extractor.mp4.Mp4Extractor
|
||||||
|
import androidx.media3.extractor.ogg.OggExtractor
|
||||||
|
import androidx.media3.extractor.wav.WavExtractor
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
object PlayerModule {
|
||||||
|
@Provides
|
||||||
|
fun provideAudioSink(context: Context): AudioSink {
|
||||||
|
return DefaultAudioSink.Builder(context)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideRenderersFactory(context: Context, audioSink: AudioSink): RenderersFactory {
|
||||||
|
return RenderersFactory { eventHandler,
|
||||||
|
videoRendererEventListener,
|
||||||
|
audioRendererEventListener,
|
||||||
|
textRendererOutput,
|
||||||
|
metadataRendererOutput ->
|
||||||
|
arrayOf(
|
||||||
|
MediaCodecVideoRenderer(
|
||||||
|
context,
|
||||||
|
MediaCodecSelector.DEFAULT,
|
||||||
|
DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS,
|
||||||
|
eventHandler,
|
||||||
|
videoRendererEventListener,
|
||||||
|
DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY
|
||||||
|
),
|
||||||
|
MediaCodecAudioRenderer(
|
||||||
|
context,
|
||||||
|
MediaCodecSelector.DEFAULT,
|
||||||
|
eventHandler,
|
||||||
|
audioRendererEventListener,
|
||||||
|
audioSink
|
||||||
|
),
|
||||||
|
TextRenderer(
|
||||||
|
textRendererOutput,
|
||||||
|
eventHandler.looper
|
||||||
|
),
|
||||||
|
MetadataRenderer(
|
||||||
|
metadataRendererOutput,
|
||||||
|
eventHandler.looper
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideExtractorsFactory(): ExtractorsFactory {
|
||||||
|
// Extractors order is optimized according to
|
||||||
|
// https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ
|
||||||
|
return ExtractorsFactory {
|
||||||
|
arrayOf(
|
||||||
|
FlacExtractor(),
|
||||||
|
WavExtractor(),
|
||||||
|
Mp4Extractor(),
|
||||||
|
FragmentedMp4Extractor(),
|
||||||
|
OggExtractor(),
|
||||||
|
MatroskaExtractor(),
|
||||||
|
Mp3Extractor()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideDataSourceFactory(context: Context, okHttpClient: OkHttpClient): DataSource.Factory {
|
||||||
|
return DefaultDataSource.Factory(context, OkHttpDataSource.Factory(okHttpClient))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideMediaSourceFactory(
|
||||||
|
dataSourceFactory: DataSource.Factory,
|
||||||
|
extractorsFactory: ExtractorsFactory
|
||||||
|
): MediaSource.Factory {
|
||||||
|
// Only progressive download is supported for Mastodon attachments
|
||||||
|
return ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideExoPlayer(
|
||||||
|
context: Context,
|
||||||
|
renderersFactory: RenderersFactory,
|
||||||
|
mediaSourceFactory: MediaSource.Factory
|
||||||
|
): ExoPlayer {
|
||||||
|
return ExoPlayer.Builder(context, renderersFactory, mediaSourceFactory)
|
||||||
|
.setLooper(Looper.getMainLooper())
|
||||||
|
.setHandleAudioBecomingNoisy(true) // automatically pause when unplugging headphones
|
||||||
|
.setWakeMode(C.WAKE_MODE_NONE) // playback is always in the foreground
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,9 +17,7 @@ package com.keylesspalace.tusky.fragment
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import androidx.annotation.OptIn
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.media3.common.util.UnstableApi
|
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity
|
import com.keylesspalace.tusky.ViewMediaActivity
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
|
|
||||||
|
@ -49,7 +47,6 @@ abstract class ViewMediaFragment : Fragment() {
|
||||||
protected val ARG_SINGLE_IMAGE_URL = "singleImageUrl"
|
protected val ARG_SINGLE_IMAGE_URL = "singleImageUrl"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@OptIn(UnstableApi::class)
|
|
||||||
fun newInstance(
|
fun newInstance(
|
||||||
attachment: Attachment,
|
attachment: Attachment,
|
||||||
shouldStartPostponedTransition: Boolean
|
shouldStartPostponedTransition: Boolean
|
||||||
|
|
|
@ -20,7 +20,6 @@ import android.animation.AnimatorListenerAdapter
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
@ -35,14 +34,13 @@ import android.widget.FrameLayout
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.view.GestureDetectorCompat
|
import androidx.core.view.GestureDetectorCompat
|
||||||
|
import androidx.media3.common.AudioAttributes
|
||||||
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.datasource.DefaultDataSource
|
|
||||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|
||||||
import androidx.media3.exoplayer.util.EventLogger
|
import androidx.media3.exoplayer.util.EventLogger
|
||||||
import androidx.media3.ui.AspectRatioFrameLayout
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
@ -59,17 +57,17 @@ import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Provider
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
|
|
||||||
@UnstableApi
|
@OptIn(UnstableApi::class)
|
||||||
class ViewVideoFragment : ViewMediaFragment(), Injectable {
|
class ViewVideoFragment : ViewMediaFragment(), Injectable {
|
||||||
interface VideoActionsListener {
|
interface VideoActionsListener {
|
||||||
fun onDismiss()
|
fun onDismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var okHttpClient: OkHttpClient
|
lateinit var playerProvider: Provider<ExoPlayer>
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentViewVideoBinding::bind)
|
private val binding by viewBinding(FragmentViewVideoBinding::bind)
|
||||||
|
|
||||||
|
@ -92,8 +90,6 @@ class ViewVideoFragment : ViewMediaFragment(), Injectable {
|
||||||
/** The saved seek position, if the fragment is being resumed */
|
/** The saved seek position, if the fragment is being resumed */
|
||||||
private var savedSeekPosition: Long = 0
|
private var savedSeekPosition: Long = 0
|
||||||
|
|
||||||
private lateinit var mediaSourceFactory: DefaultMediaSourceFactory
|
|
||||||
|
|
||||||
/** Have we received at least one "READY" event? */
|
/** Have we received at least one "READY" event? */
|
||||||
private var haveStarted = false
|
private var haveStarted = false
|
||||||
|
|
||||||
|
@ -106,9 +102,6 @@ class ViewVideoFragment : ViewMediaFragment(), Injectable {
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
|
|
||||||
mediaSourceFactory = DefaultMediaSourceFactory(context)
|
|
||||||
.setDataSourceFactory(DefaultDataSource.Factory(context, OkHttpDataSource.Factory(okHttpClient)))
|
|
||||||
|
|
||||||
videoActionsListener = context as VideoActionsListener
|
videoActionsListener = context as VideoActionsListener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,53 +278,18 @@ class ViewVideoFragment : ViewMediaFragment(), Injectable {
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
if (Build.VERSION.SDK_INT > 23) {
|
|
||||||
initializePlayer()
|
|
||||||
binding.videoView.onResume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
initializePlayer()
|
||||||
super.onResume()
|
binding.videoView.onResume()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT <= 23 || player == null) {
|
|
||||||
initializePlayer()
|
|
||||||
|
|
||||||
binding.videoView.onResume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun releasePlayer() {
|
|
||||||
player?.let {
|
|
||||||
savedSeekPosition = it.currentPosition
|
|
||||||
it.release()
|
|
||||||
player = null
|
|
||||||
binding.videoView.player = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
|
|
||||||
// If <= API 23 then multi-window mode is not available, so this is a good time to
|
|
||||||
// pause everything
|
|
||||||
if (Build.VERSION.SDK_INT <= 23) {
|
|
||||||
binding.videoView.onPause()
|
|
||||||
releasePlayer()
|
|
||||||
handler.removeCallbacks(hideToolbar)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
|
|
||||||
// If > API 23 then this might be multi-window, and definitely wasn't paused in onPause,
|
// This might be multi-window, so pause everything now.
|
||||||
// so pause everything now.
|
binding.videoView.onPause()
|
||||||
if (Build.VERSION.SDK_INT > 23) {
|
releasePlayer()
|
||||||
binding.videoView.onPause()
|
handler.removeCallbacks(hideToolbar)
|
||||||
releasePlayer()
|
|
||||||
handler.removeCallbacks(hideToolbar)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
@ -340,18 +298,22 @@ class ViewVideoFragment : ViewMediaFragment(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializePlayer() {
|
private fun initializePlayer() {
|
||||||
ExoPlayer.Builder(requireContext())
|
player = playerProvider.get().apply {
|
||||||
.setMediaSourceFactory(mediaSourceFactory)
|
setAudioAttributes(
|
||||||
.build().apply {
|
AudioAttributes.Builder()
|
||||||
if (BuildConfig.DEBUG) addAnalyticsListener(EventLogger("$TAG:ExoPlayer"))
|
.setContentType(if (isAudio) C.AUDIO_CONTENT_TYPE_UNKNOWN else C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||||
setMediaItem(MediaItem.fromUri(mediaAttachment.url))
|
.setUsage(C.USAGE_MEDIA)
|
||||||
addListener(mediaPlayerListener)
|
.build(),
|
||||||
repeatMode = Player.REPEAT_MODE_ONE
|
true
|
||||||
playWhenReady = true
|
)
|
||||||
seekTo(savedSeekPosition)
|
if (BuildConfig.DEBUG) addAnalyticsListener(EventLogger("$TAG:ExoPlayer"))
|
||||||
prepare()
|
setMediaItem(MediaItem.fromUri(mediaAttachment.url))
|
||||||
player = this
|
addListener(mediaPlayerListener)
|
||||||
}
|
repeatMode = Player.REPEAT_MODE_ONE
|
||||||
|
playWhenReady = true
|
||||||
|
seekTo(savedSeekPosition)
|
||||||
|
prepare()
|
||||||
|
}
|
||||||
|
|
||||||
binding.videoView.player = player
|
binding.videoView.player = player
|
||||||
|
|
||||||
|
@ -378,6 +340,15 @@ class ViewVideoFragment : ViewMediaFragment(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun releasePlayer() {
|
||||||
|
player?.let {
|
||||||
|
savedSeekPosition = it.currentPosition
|
||||||
|
it.release()
|
||||||
|
player = null
|
||||||
|
binding.videoView.player = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun setupMediaView(
|
override fun setupMediaView(
|
||||||
url: String,
|
url: String,
|
||||||
|
|
|
@ -79,9 +79,6 @@ androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-lived
|
||||||
androidx-lifecycle-reactivestreams-ktx = { module = "androidx.lifecycle:lifecycle-reactivestreams-ktx", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-reactivestreams-ktx = { module = "androidx.lifecycle:lifecycle-reactivestreams-ktx", version.ref = "androidx-lifecycle" }
|
||||||
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
|
||||||
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" }
|
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" }
|
||||||
androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "androidx-media3" }
|
|
||||||
androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "androidx-media3" }
|
|
||||||
androidx-media3-exoplayer-rtsp = { module = "androidx.media3:media3-exoplayer-rtsp", version.ref = "androidx-media3" }
|
|
||||||
androidx-media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "androidx-media3" }
|
androidx-media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "androidx-media3" }
|
||||||
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" }
|
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" }
|
||||||
androidx-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "androidx-paging" }
|
androidx-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "androidx-paging" }
|
||||||
|
@ -142,8 +139,8 @@ androidx = ["androidx-core-ktx", "androidx-appcompat", "androidx-fragment-ktx",
|
||||||
"androidx-emoji2-core", "androidx-emoji2-views-core", "androidx-emoji2-view-helper", "androidx-lifecycle-viewmodel-ktx",
|
"androidx-emoji2-core", "androidx-emoji2-views-core", "androidx-emoji2-view-helper", "androidx-lifecycle-viewmodel-ktx",
|
||||||
"androidx-lifecycle-livedata-ktx", "androidx-lifecycle-common-java8", "androidx-lifecycle-reactivestreams-ktx",
|
"androidx-lifecycle-livedata-ktx", "androidx-lifecycle-common-java8", "androidx-lifecycle-reactivestreams-ktx",
|
||||||
"androidx-constraintlayout", "androidx-paging-runtime-ktx", "androidx-viewpager2", "androidx-work-runtime-ktx",
|
"androidx-constraintlayout", "androidx-paging-runtime-ktx", "androidx-viewpager2", "androidx-work-runtime-ktx",
|
||||||
"androidx-core-splashscreen", "androidx-activity", "androidx-media3-exoplayer", "androidx-media3-exoplayer-dash",
|
"androidx-core-splashscreen", "androidx-activity", "androidx-media3-exoplayer", "androidx-media3-datasource-okhttp",
|
||||||
"androidx-media3-exoplayer-hls", "androidx-media3-exoplayer-rtsp", "androidx-media3-datasource-okhttp", "androidx-media3-ui"]
|
"androidx-media3-ui"]
|
||||||
dagger = ["dagger-core", "dagger-android-core", "dagger-android-support"]
|
dagger = ["dagger-core", "dagger-android-core", "dagger-android-support"]
|
||||||
dagger-processors = ["dagger-compiler", "dagger-android-processor"]
|
dagger-processors = ["dagger-compiler", "dagger-android-processor"]
|
||||||
filemojicompat = ["filemojicompat-core", "filemojicompat-ui", "filemojicompat-defaults"]
|
filemojicompat = ["filemojicompat-core", "filemojicompat-ui", "filemojicompat-defaults"]
|
||||||
|
|
Loading…
Reference in a new issue