I'm using the more-or-less default ExoPlayer 2.1 SimpleExoPlayer to stream from either a url Uri or a local file Uri. The Uri is loaded into the MediaSource,
MediaSource mediaSource = new ExtractorMediaSource(url,
dataSourceFactory, extractorsFactory, null, null);
The source is sometimes a Video, mp4 instead of mp3, so in the Activity I set it to the com.google.android.exoplayer2.ui.SimpleExoPlayerView
mVideoView = (SimpleExoPlayerView) findViewById(R.id.media_player);
mVideoView.setPlayer(player);
mVideoView.requestFocus();
I've read in an article:
SimpleExoPlayerView has also been updated to look at ID3 metadata, and will automatically display embedded ID3 album art during audio playbacks. If not desired, this functionality can be disabled using SimpleExoPlayerView’s setUseArtwork method.
I've seen it answered for Files, How to get and set (change) ID3 tag (metadata) of audio files?,
But I'm hoping to set the ID3 metadata for a Uri derived from a url String. Is it possible? Otherwise, is it possible to set the artwork for an ExoPlayerView without editing the ID3 metafiles? Or possible to change the ID3 meta for a File without a dependency?
Edit
So I've found an Issue which says this is solved, and the issuer linked to an override of the exo_simple_player_view here
I've found in the blog post
When a SimpleExoPlayerView is instantiated it inflates its layout from the layout file exo_simple_player_view.xml. PlaybackControlView inflates its layout from exo_playback_control_view.xml. To customize these layouts, an application can define layout files with the same names in its own res/layout* directories. These layout files override the ones provided by the ExoPlayer library.
So I have to override the simpleview somehow.
You can use this function and pass mediaUri and thunbnailUri to it
private fun PlayerView.loadArtWorkIfMp3(mediaUri: Uri, thumbnailUri: Uri) {
try {
val imageView = this.findViewById<ImageView>(R.id.exo_artwork)
if (mediaUri.lastPathSegment!!.contains("mp3")) {
this.useArtwork = true
imageView.scaleType = ImageView.ScaleType.CENTER_INSIDE
imageView.loadImage(thumbnailUri) {
this.defaultArtwork = it
}
}
} catch (e: Exception) {
Log.d("artwork", "exo_artwork not found")
}
}
and You will use loadImage function
#BindingAdapter(value = ["imageUri", "successCallback"], requireAll = false)
fun ImageView.loadImage(imgUrl: Uri?, onLoadSuccess: (resource: Drawable) -> Unit = {}) {
val requestOption = RequestOptions()
.placeholder(R.drawable.ic_music)
.error(R.drawable.ic_music)
Glide.with(this.context)
.load(imgUrl)
.transition(DrawableTransitionOptions.withCrossFade())
.apply(requestOption)
.centerInside()
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.listener(GlideImageRequestListener(object : GlideImageRequestListener.Callback {
override fun onFailure(message: String?) {
Log.d("loadImage", "onFailure:-> $message")
}
override fun onSuccess(dataSource: String, resource: Drawable) {
Log.d("loadImage", "onSuccess:-> load from $dataSource")
onLoadSuccess(resource)
}
}))
.into(this)
}
And last one
class GlideImageRequestListener(private val callback: Callback? = null) : RequestListener<Drawable> {
interface Callback {
fun onFailure(message: String?)
fun onSuccess(dataSource: String, resource: Drawable)
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
callback?.onFailure(e?.message)
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
resource?.let {
target?.onResourceReady(
it,
DrawableCrossFadeTransition(1000, isFirstResource)
)
}
callback?.onSuccess(dataSource.toString(),resource!!)
return true
}
}
Related
I'm trying to solve a problem with getting the dominant color in an image, but I can't convert it to a bitmap, because I get null all the time. What could the problem be?
View itself in XML:
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/ivIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintCircleRadius="8dp"
app:srcCompat="#drawable/icon_default"
tools:srcCompat="#drawable/icon_default" />
Getting the image goes through Glide by loading the image by url (fun loadInto (...)) and after that I try to get the drawable:
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewOutput.iconFlow.collect { photoUrl ->
if (url != null) {
loadInto(url, binding.Icon)
getDominantColor(binding.ivIcon) <- null here
}
}
}
Update:
private fun getDominantColor(image: AppCompatImageView) {
val bitmap: Bitmap = (image.drawable as BitmapDrawable).bitmap
Palette.from(bitmap).generate { palette ->
val dom: Int = palette!!.getDominantColor(0x000000)
setGradientColor(dom)
}
}
fun loadInto(
url: String,
imageView: ImageView,
placeHolder: Int = 0,
errorHolder: Int = 0,
) {
Glide
.with(imageView)
.load(url)
.run { if (placeHolder != 0) placeholder(placeHolder) else this }
.run { if (errorHolder != 0) error(errorHolder) else this }
.into(imageView)
}
As mentioned by #Lino, Glide will make an asynchronous call to get the image. This will take some time and it may return null if you get the Bitmap from the ImageView right after it.
Your attempt to create a listener callback is correct. And if it is properly implemented, onResourceReady() method should be called once ready. You may refer the sample below:
Glide
.with(this#MainActivity)
.load(url)
.run { this }
.run { this }
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
// Process with your bitmap in callback here
Palette.from((resource!! as BitmapDrawable).bitmap).generate { palette ->
val dom: Int = palette!!.getDominantColor(0x000000)
setGradientColor(dom)
}
return false
}
})
.into(ivIcon)
I need to download multiple images, and after all downloads are completed (outside of the Main Thread), perform other actions in the activity.
I am currently using Glide to download as follows:
ImageDownloader.kt
class ImageDownloader {
fun downloadPack(context: Context, path: String, pack: PackModel) {
for (image: ImageModel in pack.images) {
Glide.with(context)
.asBitmap()
.load(image.imageFileUrl)
.listener(object : RequestListener<Bitmap> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Bitmap>?, isFirstResource: Boolean): Boolean {
return false
}
override fun onResourceReady(bitmap: Bitmap?, model: Any?, target: Target<Bitmap>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
saveImage(path, pack.id, image.imageFileName, bitmap!!)
return true
}
}).submit()
}
}
private fun saveImage(path: String, id: String, fileName: String, bitmap: Bitmap) {
val dir = File(path + id)
if (!dir.exists()) dir.mkdirs()
val file = File(dir, fileName)
val out = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 75, out)
out.flush()
out.close()
// to check the download progress for each image in logcat
println("done: $fileName")
}
}
In the activity I call this method inside a CoroutineScope as follows:
PackActivity.kt
class PackActivity: AppCompatActivity() {
private lateinit var bind: ActivityPackBinding
private lateinit var path: String
private lateinit var pack: PackModel
// other basic codes
override fun onCreate(savedInstanceState: Bundle?) {
// other basic codes
path = "$filesDir/images_asset/"
pack = intent.getParcelableExtra(PACK_DATA)!!
bind.buttonDownload.setOnClickListener {
downloadPack()
}
}
private fun downloadPack() {
CoroutineScope(Dispatchers.IO).launch {
val async = async {
ImageDownloader().downloadPack(applicationContext, path, pack)
}
val result = async.await()
withContext(Dispatchers.Main) {
result.apply {
println("finished")
// other things todo
}
}
}
}
}
I am trying to proceed with other actions after downloading all the images in PackActivity.kt, but as a result, using println("finished") and checking the logcat, the code is starting even before the first download starts...
Some information:
PackModel and ImageModel are my data class, where PackModel has the Id of each pack and a list of ImageModel, which in turn has the ImageFileName and ImageFileUrl. All data is obtained from a web request.
I want the images to be saved in the folder data/data/AppPackageName/files/images_asset/PackID/... And with the tests I did, I was unable to use DownloadManager directing the images to this internal folder of the App, that's why I'm using Glide.
The first, crucial step is to adapt the async Glide request into a suspend fun using suspendCancellableCoroutine. Here's how:
private suspend fun downloadBitmap(
context: Context,
image: ImageModel
) = suspendCancellableCoroutine<Bitmap> { cont ->
Glide.with(context)
.asBitmap()
.load(image.imageFileUrl)
.listener(object : RequestListener<Bitmap> {
override fun onLoadFailed(
e: GlideException, model: Any?,
target: Target<Bitmap>?, isFirstResource: Boolean
): Boolean {
cont.resumeWith(Result.failure(e))
return false
}
override fun onResourceReady(
bitmap: Bitmap, model: Any?, target: Target<Bitmap>?,
dataSource: DataSource?, isFirstResource: Boolean
): Boolean {
cont.resumeWith(Result.success(bitmap))
return false
}
}).submit()
}
With that done, now you'll have an easy time of making concurrent downloads and awaiting on all of them:
class ImageDownloader {
suspend fun downloadPack(context: Context, path: String, pack: PackModel) {
coroutineScope {
for (image: ImageModel in pack.images) {
launch {
val bitmap = downloadBitmap(context, image)
saveImage(path, pack.id, image.imageFileName, bitmap)
}
}
}
}
private suspend fun saveImage(
path: String, id: String, imageFileName: String, bitmap: Bitmap
) {
withContext(IO) {
// your code
}
}
}
Watch carefully which dispatchers I use above: using the Main dispatcher for everything except saveImage, which is the only place that contains code that is actually blocking.
Finally, to use everything, this is all you need:
private fun downloadPack() {
GlobalScope.launch {
ImageDownloader().downloadPack(applicationContext, path, pack)
println("finished")
// other things todo
}
}
Again, everything on the Main dispatcher because the blocking code is safely corralled into the IO dispatcher.
I use GlobalScope above for the lack of knowledge of your larger context, but it's probably a bad idea. Writing CoroutineScope(IO).launch has all the same problems, plus allocating several more objects.
Think twice about what's going to happen to your downloads if the user navigates away from the app, or if they navigate away and back repeatedly, triggering a growing pile of background downloads. In the code above, i didn't treat cancellation within suspendCancellableCoroutine because I'm not that intimate with Glide. You should add a cont.onCancellation handler to be correct.
I am trying to change my radiobutton.buttonDrawable inside my adapter. Selecting static drawables from my drawable folder works. What I now want is to download the drawables (.svg files) from my cloud-firestore-storage, convert them to a drawable and set them to the radiobutton.
My current implementation doesn't work as it just does nothing (doesn't change anything). I've already checked the urls (getItem(position).icon) and they seem correct.
Current approach
class ShopFilterItemAdapter #Inject constructor(
// #ApplicationContext private val context: Context
) : ListAdapter<ShopFilterCategory, ShopFilterItemAdapter.ShopFilterViewHolder>(Companion) {
private var checkedRadioButton: CompoundButton? = null
var defaultCheckedId: Int = 0
private lateinit var listener: OnItemSelectedListener
companion object: DiffUtil.ItemCallback<ShopFilterCategory>() {
override fun areItemsTheSame(oldItem: ShopFilterCategory, newItem: ShopFilterCategory): Boolean = oldItem === newItem
override fun areContentsTheSame(oldItem: ShopFilterCategory, newItem: ShopFilterCategory): Boolean = oldItem == newItem
}
inner class ShopFilterViewHolder(val binding: ShopFilterListItemBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShopFilterViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ShopFilterListItemBinding.inflate(layoutInflater, parent, false)
return ShopFilterViewHolder(binding)
}
override fun onBindViewHolder(holder: ShopFilterViewHolder, position: Int) {
holder.binding.filterItem = getItem(position)
if (checkedRadioButton == null && defaultCheckedId == position) holder.binding.rbItem.isChecked = true
if (holder.binding.rbItem.isChecked) checkedRadioButton = holder.binding.rbItem
// Trying to load the images here
Glide.with(holder.binding.root)
.asDrawable()
.load(getItem(position).icon)
.into(object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
holder.binding.rbItem.buttonDrawable = resource
}
override fun onLoadCleared(placeholder: Drawable?) {
TODO("Not yet implemented")
}
})
holder.binding.executePendingBindings()
}
EDIT
I was partially wrong, it throws an error:
java.io.IOException: java.lang.RuntimeException: setDataSourceCallback failed: status = 0x80000000
at com.bumptech.glide.load.resource.bitmap.VideoDecoder.decode(VideoDecoder.java:185)
at com.bumptech.glide.load.engine.DecodePath.decodeResourceWithList(DecodePath.java:92)
at com.bumptech.glide.load.engine.DecodePath.decodeResource(DecodePath.java:70)
at com.bumptech.glide.load.engine.DecodePath.decode(DecodePath.java:59)
at com.bumptech.glide.load.engine.LoadPath.loadWithExceptionList(LoadPath.java:76)
at com.bumptech.glide.load.engine.LoadPath.load(LoadPath.java:57)
at com.bumptech.glide.load.engine.DecodeJob.runLoadPath(DecodeJob.java:524)
at com.bumptech.glide.load.engine.DecodeJob.decodeFromFetcher(DecodeJob.java:488)
at com.bumptech.glide.load.engine.DecodeJob.decodeFromData(DecodeJob.java:474)
at com.bumptech.glide.load.engine.DecodeJob.decodeFromRetrievedData(DecodeJob.java:426)
at com.bumptech.glide.load.engine.DecodeJob.onDataFetcherReady(DecodeJob.java:390)
at com.bumptech.glide.load.engine.SourceGenerator.onDataFetcherReady(SourceGenerator.java:176)
at com.bumptech.glide.load.engine.DataCacheGenerator.onDataReady(DataCacheGenerator.java:94)
at com.bumptech.glide.load.model.ByteBufferFileLoader$ByteBufferFetcher.loadData(ByteBufferFileLoader.java:70)
at com.bumptech.glide.load.engine.DataCacheGenerator.startNext(DataCacheGenerator.java:74)
at com.bumptech.glide.load.engine.SourceGenerator.startNext(SourceGenerator.java:50)
at com.bumptech.glide.load.engine.DecodeJob.runGenerators(DecodeJob.java:310)
at com.bumptech.glide.load.engine.DecodeJob.runWrapped(DecodeJob.java:279)
at com.bumptech.glide.load.engine.DecodeJob.run(DecodeJob.java:234)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
at com.bumptech.glide.load.engine.executor.GlideExecutor$DefaultThreadFactory$1.run(GlideExecutor.java:393)
Caused by: java.lang.RuntimeException: setDataSourceCallback failed: status = 0x80000000
at android.media.MediaMetadataRetriever._setDataSource(Native Method)
at android.media.MediaMetadataRetriever.setDataSource(MediaMetadataRetriever.java:210)
at com.bumptech.glide.load.resource.bitmap.VideoDecoder$ByteBufferInitializer.initialize(VideoDecoder.java:316)
at com.bumptech.glide.load.resource.bitmap.VideoDecoder$ByteBufferInitializer.initialize(VideoDecoder.java:310)
at com.bumptech.glide.load.resource.bitmap.VideoDecoder.decode(VideoDecoder.java:173)
at com.bumptech.glide.load.engine.DecodePath.decodeResourceWithList(DecodePath.java:92)
at com.bumptech.glide.load.engine.DecodePath.decodeResource(DecodePath.java:70)
at com.bumptech.glide.load.engine.DecodePath.decode(DecodePath.java:59)
at com.bumptech.glide.load.engine.LoadPath.loadWithExceptionList(LoadPath.java:76)
at com.bumptech.glide.load.engine.LoadPath.load(LoadPath.java:57)
at com.bumptech.glide.load.engine.DecodeJob.runLoadPath(DecodeJob.java:524)
at com.bumptech.glide.load.engine.DecodeJob.decodeFromFetcher(DecodeJob.java:488)
at com.bumptech.glide.load.engine.DecodeJob.decodeFromData(DecodeJob.java:474)
at com.bumptech.glide.load.engine.DecodeJob.decodeFromRetrievedData(DecodeJob.java:426)
at com.bumptech.glide.load.engine.DecodeJob.onDataFetcherReady(DecodeJob.java:390)
at com.bumptech.glide.load.engine.SourceGenerator.onDataFetcherReady(SourceGenerator.java:176)
at com.bumptech.glide.load.engine.DataCacheGenerator.onDataReady(DataCacheGenerator.java:94)
at com.bumptech.glide.load.model.ByteBufferFileLoader$ByteBufferFetcher.loadData(ByteBufferFileLoader.java:70)
at com.bumptech.glide.load.engine.DataCacheGenerator.startNext(DataCacheGenerator.java:74)
at com.bumptech.glide.load.engine.SourceGenerator.startNext(SourceGenerator.java:50)
at com.bumptech.glide.load.engine.DecodeJob.runGenerators(DecodeJob.java:310)
at com.bumptech.glide.load.engine.DecodeJob.runWrapped(DecodeJob.java:279)
at com.bumptech.glide.load.engine.DecodeJob.run(DecodeJob.java:234)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
at com.bumptech.glide.load.engine.executor.GlideExecutor$DefaultThreadFactory$1.run(GlideExecutor.java:393)
I've managed to solve this problem, unfortunately it is not possible with glide, but with coil.
1. Add the necessary dependencies
implementation "io.coil-kt:coil-base:1.1.0" // or "io.coil-kt:coil-1.1.0" if you don't want to use dependency Injection
implementation "io.coil-kt:coil-svg:1.1.0"
2. Construct the ImageLoader Singleton and add the SVGEndoder
#Provides
#Singleton
fun provideImageLoader(#ApplicationContext context: Context) = ImageLoader.Builder(context)
.componentRegistry { add(SvgDecoder(context)) }
.build()
3.Build the request
fun buildRequest(context: Context, data: Any, target: RadioButton) = ImageRequest.Builder(context)
.data(data)
.target { drawable -> target.buttonDrawable = drawable }
.size(64, 60) // unfortunately I was not able to get the default radiobutton.drawable size here
.build()
4. Inside the Adapter
override fun onBindViewHolder(holder: YourHolder, position: Int) {
val newRequest = shopFilterValidator.buildRequest(
context = holder.binding.rbItem.context,
data = getItem(position).icon, // here a url from firebase
target = holder.binding.rbItem
)
imageLoader.enqueue(newRequest)
}
This will load the svg images as drawables and put them as the radiobutton.drawable
Loading an image through data binding is easy. I am using Glide in my project. I have to set placeholder image which will change as per some selection by user. Can we use some expression which accepts imageurl and placeHolder image reference.
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/vehicle_1_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/margin_twenty"
app:layout_constraintEnd_toEndOf="#id/centerGuideline"
app:layout_constraintStart_toStartOf="#id/centerGuideline"
app:layout_constraintTop_toBottomOf="#id/txt_enter_vehicle_name"
app:loadImage="#{viewModel.imgUrl}" />
#BindingAdapter({"loadImage"})
public static void loadUrlImage(ImageView view, String url, int placeHolderImage){
ImageLoaderUtil.getInstance().loadImageWithCache(view, url, placeHolderImage);
}
public void loadImageWithCache(ImageView imageView, String url, int placeholderImage) {
Glide.with(imageView.getContext())
.load(url)
.apply(getDefaultGlideOptions())
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.placeholder(placeholderImage)
.into(imageView);
}
Found this nice article: https://ayusch.com/databinding-with-glide-android/
We can also accept multiple arguments in our bindingadapter. For example, one may need to load an error image, or a placeholder while our image loads.
So I think listeners is the answer. Posting also the code in case the link is dead.
companion object {
#JvmStatic
#BindingAdapter(value = ["profileImage", "error"], requireAll = false)
fun loadImage(view: ImageView, profileImage: String, error: Int) {
Glide.with(view.context)
.load(profileImage)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
view.setImageResource(error)
return true
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
view.setImageDrawable(resource)
return true
}
})
.into(view)
}
}
and in your layout:
app:error="#{user.errorImage}"
You can add multiple parameter in BindingAdapter just like this.
#BindingAdapter("url","placeHolderImage")
public static void loadUrlImage(ImageView view, String url, int placeHolderImage)
{
ImageLoaderUtil.getInstance().loadImageWithCache(view, url, placeHolderImage);
}
And you have to add field in Imageview xml just like this.
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/vehicle_1_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/margin_twenty"
app:layout_constraintEnd_toEndOf="#id/centerGuideline"
app:layout_constraintStart_toStartOf="#id/centerGuideline"
app:layout_constraintTop_toBottomOf="#id/txt_enter_vehicle_name"
app:url="#{viewModel.imgUrl}"
app:placeHolderImage="#{viewModel.}"
/>
You have pass two thing xml Url and Placeholder.
I am developing an android application which plays videos from our server uploaded by both iphone and android, and the problem is that the videos recorded by iphone is in .mov format, which is not supported by android. I searched a lot, but couldn't found any solution. Help me out. Thanks in advance :)
I will recomend you to change the format from the iphones to a compatible (even open will be better), because 2 differents formats of videos on server soon or later will be a headache.
If you could, reformat your videos with ffmpeg and save it all with the same format on server.
If you can't or it's really hard to achieve, you could try the ExoPlayer component from Google.
I have tried on an app like you, where devices (iPhone and Android) record videos and upload to the server.
Reformatting all this video on server side will be almost impossible so we endly make the decission to apply a solution on client side for legacy videos.
https://google.github.io/ExoPlayer/
The setup from ExoPlayer it's larger than VideoView, but it's simple to be done.
private var player: ExoPlayer = initPlayer()
private fun initPlayer(): ExoPlayer {
val bandwidthMeter = DefaultBandwidthMeter()
val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(bandwidthMeter)
val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)
return ExoPlayerFactory.newSimpleInstance(context, trackSelector)
}
fun setup() {
videoExo.setPlayer(player)
videoExo.useController = false
val dataSourceFactory = DefaultDataSourceFactory(
context,
Util.getUserAgent(context, context?.packageName), DefaultBandwidthMeter()
)
val videoSource = ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(videoUri)
player.addListener(object: Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
if (playbackState == Player.STATE_READY) {
startCallback()
}
}
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {}
override fun onSeekProcessed() {}
override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {}
override fun onPlayerError(error: ExoPlaybackException?) {}
override fun onLoadingChanged(isLoading: Boolean) {}
override fun onPositionDiscontinuity(reason: Int) {}
override fun onRepeatModeChanged(repeatMode: Int) {}
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {}
override fun onTimelineChanged(timeline: Timeline?, manifest: Any?, reason: Int) {}
})
player.prepare(videoSource)
player.playWhenReady = true
player.repeatMode = REPEAT_MODE_ALL
}
Hope this helps