I want to implement timer using Kotlin coroutines, something similar to this implemented with RxJava:
Flowable.interval(0, 5, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.map { LocalDateTime.now() }
.distinctUntilChanged { old, new ->
old.minute == new.minute
}
.subscribe {
setDateTime(it)
}
It will emit LocalDateTime every new minute.
Edit: note that the API suggested in the original answer is now marked #ObsoleteCoroutineApi:
Ticker channels are not currently integrated with structured concurrency and their api will change in the future.
You can now use the Flow API to create your own ticker flow:
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun tickerFlow(period: Duration, initialDelay: Duration = Duration.ZERO) = flow {
delay(initialDelay)
while (true) {
emit(Unit)
delay(period)
}
}
And you can use it in a way very similar to your current code:
tickerFlow(5.seconds)
.map { LocalDateTime.now() }
.distinctUntilChanged { old, new ->
old.minute == new.minute
}
.onEach {
setDateTime(it)
}
.launchIn(viewModelScope) // or lifecycleScope or other
Note: with the code as written here, the time taken to process elements is not taken into account by tickerFlow, so the delay might not be regular (it's a delay between element processing). If you want the ticker to tick independently of the processing of each element, you may want to use a buffer or a dedicated thread (e.g. via flowOn).
Original answer
I believe it is still experimental, but you may use a TickerChannel to produce values every X millis:
val tickerChannel = ticker(delayMillis = 60_000, initialDelayMillis = 0)
repeat(10) {
tickerChannel.receive()
val currentTime = LocalDateTime.now()
println(currentTime)
}
If you need to carry on doing your work while your "subscribe" does something for each "tick", you may launch a background coroutine that will read from this channel and do the thing you want:
val tickerChannel = ticker(delayMillis = 60_000, initialDelayMillis = 0)
launch {
for (event in tickerChannel) {
// the 'event' variable is of type Unit, so we don't really care about it
val currentTime = LocalDateTime.now()
println(currentTime)
}
}
delay(1000)
// when you're done with the ticker and don't want more events
tickerChannel.cancel()
If you want to stop from inside the loop, you can simply break out of it, and then cancel the channel:
val ticker = ticker(500, 0)
var count = 0
for (event in ticker) {
count++
if (count == 4) {
break
} else {
println(count)
}
}
ticker.cancel()
A very pragmatic approach with Kotlin Flows could be:
// Create the timer flow
val timer = (0..Int.MAX_VALUE)
.asSequence()
.asFlow()
.onEach { delay(1_000) } // specify delay
// Consume it
timer.collect {
println("bling: ${it}")
}
another possible solution as a reusable kotlin extension of CoroutineScope
fun CoroutineScope.launchPeriodicAsync(
repeatMillis: Long,
action: () -> Unit
) = this.async {
if (repeatMillis > 0) {
while (isActive) {
action()
delay(repeatMillis)
}
} else {
action()
}
}
and then usage as:
var job = CoroutineScope(Dispatchers.IO).launchPeriodicAsync(100) {
//...
}
and then to interrupt it:
job.cancel()
another note: we consider here that action is non-blocking and does not take time.
You can create a countdown timer like this
GlobalScope.launch(Dispatchers.Main) {
val totalSeconds = TimeUnit.MINUTES.toSeconds(2)
val tickSeconds = 1
for (second in totalSeconds downTo tickSeconds) {
val time = String.format("%02d:%02d",
TimeUnit.SECONDS.toMinutes(second),
second - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(second))
)
timerTextView?.text = time
delay(1000)
}
timerTextView?.text = "Done!"
}
Here's a possible solution using Kotlin Flow
fun tickFlow(millis: Long) = callbackFlow<Int> {
val timer = Timer()
var time = 0
timer.scheduleAtFixedRate(
object : TimerTask() {
override fun run() {
try { offer(time) } catch (e: Exception) {}
time += 1
}
},
0,
millis)
awaitClose {
timer.cancel()
}
}
Usage
val job = CoroutineScope(Dispatchers.Main).launch {
tickFlow(125L).collect {
print(it)
}
}
...
job.cancel()
Edit: Joffrey has edited his solution with a better approach.
Old :
Joffrey's solution works for me but I ran into a problem with the for loop.
I have to cancel my ticker in the for loop like this :
val ticker = ticker(500, 0)
for (event in ticker) {
if (...) {
ticker.cancel()
} else {
...
}
}
}
But ticker.cancel() was throwing a cancellationException because the for loop kept going after this.
I had to use a while loop to check if the channel was not closed to not get this exception.
val ticker = ticker(500, 0)
while (!ticker.isClosedForReceive && ticker.iterator().hasNext()) {
if (...) {
ticker.cancel()
} else {
...
}
}
}
Timer with START, PAUSE and STOP functions.
Usage:
val timer = Timer(millisInFuture = 10_000L, runAtStart = false)
timer.start()
Timer class:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
enum class PlayerMode {
PLAYING,
PAUSED,
STOPPED
}
class Timer(
val millisInFuture: Long,
val countDownInterval: Long = 1000L,
runAtStart: Boolean = false,
val onFinish: (() -> Unit)? = null,
val onTick: ((Long) -> Unit)? = null
) {
private var job: Job = Job()
private val _tick = MutableStateFlow(0L)
val tick = _tick.asStateFlow()
private val _playerMode = MutableStateFlow(PlayerMode.STOPPED)
val playerMode = _playerMode.asStateFlow()
private val scope = CoroutineScope(Dispatchers.Default)
init {
if (runAtStart) start()
}
fun start() {
if (_tick.value == 0L) _tick.value = millisInFuture
job.cancel()
job = scope.launch(Dispatchers.IO) {
_playerMode.value = PlayerMode.PLAYING
while (isActive) {
if (_tick.value <= 0) {
job.cancel()
onFinish?.invoke()
_playerMode.value = PlayerMode.STOPPED
return#launch
}
delay(timeMillis = countDownInterval)
_tick.value -= countDownInterval
onTick?.invoke(this#Timer._tick.value)
}
}
}
fun pause() {
job.cancel()
_playerMode.value = PlayerMode.PAUSED
}
fun stop() {
job.cancel()
_tick.value = 0
_playerMode.value = PlayerMode.STOPPED
}
}
I took inspiration from here.
Here is Flow version of Observable.intervalRange(1, 5, 0, 1, TimeUnit.SECONDS) based on Joffrey's answer:
fun tickerFlow(start: Long,
count: Long,
initialDelayMs: Long,
periodMs: Long) = flow<Long> {
delay(initialDelayMs)
var counter = start
while (counter <= count) {
emit(counter)
counter += 1
delay(periodMs)
}
}
//...
tickerFlow(1, 5, 0, 1_000L)
Made a copy of Observable.intervalRange(0, 90, 0, 1, TimeUnit.SECONDS) ( will emit item in 90 sec each 1 sec ):
fun intervalRange(start: Long, count: Long, initialDelay: Long = 0, period: Long, unit: TimeUnit): Flow<Long> {
return flow<Long> {
require(count >= 0) { "count >= 0 required but it was $count" }
require(initialDelay >= 0) { "initialDelay >= 0 required but it was $initialDelay" }
require(period > 0) { "period > 0 required but it was $period" }
val end = start + (count - 1)
require(!(start > 0 && end < 0)) { "Overflow! start + count is bigger than Long.MAX_VALUE" }
if (initialDelay > 0) {
delay(unit.toMillis(initialDelay))
}
var counter = start
while (counter <= count) {
emit(counter)
counter += 1
delay(unit.toMillis(period))
}
}
}
Usage:
lifecycleScope.launch {
intervalRange(0, 90, 0, 1, TimeUnit.SECONDS)
.onEach {
Log.d(TAG, "intervalRange: ${90 - it}")
}
.lastOrNull()
}
Used this recently to chunk values based on a timer and max buffer size.
private object Tick
#Suppress("UNCHECKED_CAST")
fun <T : Any> Flow<T>.chunked(size: Int, initialDelay: Long, delay: Long): Flow<List<T>> = flow {
if (size <= 0) throw IllegalArgumentException("invalid chunk size $size - expected > 0")
val chunkedList = mutableListOf<T>()
if (delay > 0L) {
merge(this#chunked, timerFlow(initialDelay, delay, Tick))
} else {
this#chunked
}
.collect {
when (it) {
is Tick -> {
if (chunkedList.isNotEmpty()) {
emit(chunkedList.toList())
chunkedList.clear()
}
}
else -> {
chunkedList.add(it as T)
if (chunkedList.size >= size) {
emit(chunkedList.toList())
chunkedList.clear()
}
}
}
}
if (chunkedList.isNotEmpty()) {
emit(chunkedList.toList())
}
}
fun <T> timerFlow(initialDelay: Long, delay: Long, o: T) = flow {
if (delay <= 0) throw IllegalArgumentException("invalid delay $delay - expected > 0")
if (initialDelay > 0) delay(initialDelay)
while (currentCoroutineContext().isActive) {
emit(o)
delay(delay)
}
}
It's not using Kotlin coroutines, but if your use case is simple enough you can always just use something like a fixedRateTimer or timer (docs here) which resolve to JVM native Timer.
I was using RxJava's interval for a relatively simple scenario and when I switched to using Timers I saw significant performance and memory improvements.
You can also run your code on the main thread on Android by using View.post() or it's mutliple variants.
The only real annoyance is you'll need to keep track of the old time's state yourself instead of relying on RxJava to do it for you.
But this will always be much faster (important if you're doing performance critical stuff like UI animations etc) and will not have the memory overhead of RxJava's Flowables.
Here's the question's code using a fixedRateTimer:
var currentTime: LocalDateTime = LocalDateTime.now()
fixedRateTimer(period = 5000L) {
val newTime = LocalDateTime.now()
if (currentTime.minute != newTime.minute) {
post { // post the below code to the UI thread to update UI stuff
setDateTime(newTime)
}
currentTime = newTime
}
}
enter image description here
enter code here
private val updateLiveShowTicker = flow {
while (true) {
emit(Unit)
delay(1000L * UPDATE_PROGRAM_INFO_INTERVAL_SECONDS)
}
}
private val updateShowProgressTicker = flow {
while (true) {
emit(Unit)
delay(1000L * UPDATE_SHOW_PROGRESS_INTERVAL_SECONDS)
}
}
private val liveShow = updateLiveShowTicker
.combine(channelId) { _, channelId -> programInfoRepository.getShow(channelId) }
.catch { emit(LiveShow(application.getString(R.string.activity_channel_detail_info_error))) }
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1)
.distinctUntilChanged()
My solution,You can now use the Flow API to create your own ticker flow:
Related
I want to avoid multiple function call when LaunchEffect key triggers.
LaunchedEffect(key1 = isEnableState, key2 = viewModel.uiState) {
viewModel.scanState(bluetoothAdapter)
}
when first composition isEnableState and viewModel.uiState both will trigger twice and call viewModel.scanState(bluetoothAdapter).
isEnableState is a Boolean type and viewModel.uiState is sealed class of UI types.
var uiState by mutableStateOf<UIState>(UIState.Initial)
private set
var isEnableState by mutableStateOf(false)
private set
So how can we handle idiomatic way to avoid duplicate calls?
Thanks
UPDATE
ContentStateful
#Composable
fun ContentStateful(
context: Context = LocalContext.current,
viewModel: ContentViewModel = koinViewModel(),
) {
LaunchedEffect(key1 = viewModel.isEnableState, key2 = viewModel.uiState) {
viewModel.scanState(bluetoothAdapter)
}
LaunchedEffect(viewModel.previous) {
viewModel.changeDeviceSate()
}
ContentStateLess{
viewModel.isEnableState = false
}
}
ContentStateLess
#Composable
fun ContentStateLess(changeAction: () -> Unit) {
Button(onClick = { changeAction() }) {
Text(text = "Click On me")
}
}
ContentViewModel
class ContentViewModel : BaseViewModel() {
var uiState by mutableStateOf<UIState>(UIState.Initial)
var isEnableState by mutableStateOf(false)
fun scanState(bluetoothAdapter: BluetoothAdapter) {
if (isEnableState && isInitialOrScanningUiState()) {
// start scanning
} else {
// stop scanning
}
}
private fun isInitialOrScanningUiState(): Boolean {
return (uiState == UIState.Initial || uiState == UIState.ScanningDevice)
}
fun changeDeviceSate() {
if (previous == BOND_NONE && newState == BONDING) {
uiState = UIState.LoadingState
} else if (previous == BONDING && newState == BONDED) {
uiState = UIState.ConnectedState(it)
} else {
uiState = UIState.ConnectionFailedState
}
}
}
scanState function is start and stop scanning of devices.
I guess the answer below would work or might require some modification to work but logic for preventing double clicks can be used only if you wish to prevent actions happen initially within time frame of small interval. To prevent double clicks you you set current time and check again if the time is above threshold to invoke click callback. In your situation also adding states with delay might solve the issue.
IDLE, BUSY, READY
var launchState by remember {mutableStateOf(IDLE)}
LaunchedEffect(key1 = isEnableState, key2 = viewModel.uiState) {
if(launchState != BUSY){
viewModel.scanState(bluetoothAdapter)
if(launchState == IDLE){ launchState = BUSY)
}
}
LaunchedEffect(launchState) {
if(launchState == BUSY){
delay(50)
launchState = READY
}
}
I'm trying to create a flow with coroutines but it's not giving to me the expected result.
What I'd like to have is giving an expiration time (doesn't matter if it's in millis, seconds, etc..) when the time arrives to 0 it stops the countdown. What I have now is :
private fun tickerFlow(start: Long, end: Long = 0L) = flow {
var count = start
while (count >= end) {
emit(Unit)
count--
delay(1_000L)
}
}
And then I call this function as :
val expireDate = LocalDateTime.now().plusSeconds(10L).toEpochSecond(ZoneOffset.UTC)
tickerFlow(expireDate)
.map { LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) - expireDate }
.distinctUntilChanged { old, new ->
old == new
}
.onEach {
//Here I should print the timer going down with this pattern
//00h: 00m: 00s I did it with String.format("%02dh: %02dm: %02ds") and it works though.
}
.onCompletion {
//Setting the text when completed
}
.launchIn(scope = scope)
But even with this test that what I'm trying is to have the expiry time as 10 seconds from now it doesn't print nor end as I would. Am I missing something? Is there any way I could emit the local date time so I have the hours, minutes and seconds? perhaps I have to do the calculus to get the seconds, minutes hours from milis / seconds.
TLDR;
I'm getting from backend an expiry date, and I want to know when this expiry date finish so I have to calculate it with the now() and check when it is expired.
You don't really need a flow here. Try this code:
val expireDate = LocalDateTime.now().plusSeconds(10L).toEpochSecond(ZoneOffset.UTC)
val currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)
for (i in (expireDate - currentTime) downTo 1) {
println("$i seconds remaining") // Format the remaining seconds however you wish
delay(1_000) // Delay for 1 second
}
println("TIME UP") // Run your completion code here
Also, this code is safe to run on main thread as delay doesn't block.
In your code, the problem is that you are passing the expireDate itself to tickerFlow. expireDate contains the time in seconds from epoch and not the seconds difference from current time. Just pass expireDate - LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) to tickerFlow and it will work.
EDIT: Complete implementation using flow
private fun tickerFlow(start: Long, end: Long = 0L) = flow {
for (i in start downTo end) {
emit(i)
delay(1_000)
}
}
val expireDate = LocalDateTime.now().plusSeconds(10L).toEpochSecond(ZoneOffset.UTC)
val currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)
tickerFlow(expireDate - currentTime)
.onEach { secondsRemaining ->
// Format and display the time
}
.onCompletion {
// Handle completion
}
.launchIn(scope)
Solution with flow
private fun countDownFlow(
start: Long,
delayInSeconds: Long = 1_000L,
) = flow {
var count = start
while (count >= 0L) {
emit(count--)
delay(delayInSeconds)
}
}
And then given an expiration date, get the current date subtract them and pass it as start.
val expireDate = LocalDateTime.now().plusSeconds(10L).toEpochSecond(ZoneOffset.UTC)
val currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)
tickerFlow(expireDate - currentTime)
.onEach {
binding.yourTimer.text = String.format(
"%02dh: %02dm: %02ds",
TimeUnit.SECONDS.toHours(it),
TimeUnit.SECONDS.toMinutes(it),
TimeUnit.SECONDS.toSeconds(it),
)
}
.onCompletion {
//Update UI
}
.launchIn(coroutineScope)
If in hurry copy/paste this!
fun getFlow( delayTimeMilliseconds: Long, startValue: Long, stepValue : Long = 1, endValue : Long =0): Flow<Long> =
( startValue downTo endValue step stepValue ).asFlow().flowOn(Dispatchers.IO)
.onEach { delay( delayTimeMilliseconds) }
.onStart { emit( startValue) }
.conflate()
.transform { remainingValue: Long ->
if(remainingValue<0) emit(0)
else emit( remainingValue)
}
On my production app I use timer as an UseCase as per clean architecture. Like this :
class TimerFlowUseCase constructor() : StateFlowUseCase<TimerFlowUseCase.Params, Long>() {
override suspend fun getFlow(params: Params): Flow<Long> =
(params.startValue downTo params.endValue step params.stepValue ).asFlow().flowOn(Dispatchers.IO)
.onEach { delay(params.delayTimeMilliseconds) }
.onStart { emit(params.startValue) } // Emits total value on start
.conflate()
.transform { remainingValue: Long ->
if(remainingValue<0) emit(0)
else emit( remainingValue)
}
data class Params( val delayTimeMilliseconds: Long, val startValue: Long, val stepValue : Long = 1, val endValue : Long =0)
}
Where superclass is:
abstract class StateFlowUseCase<P, R> {
suspend operator fun invoke(params: P , coroutineScope: CoroutineScope): StateFlow<R> {
return getFlow(params).stateIn(coroutineScope)
}
abstract suspend fun getFlow(params: P): Flow<R>
}
I would like to have ImageView change picture every 1 second. I've tried using Java-specific solutions using Handler but weren't able to translate them correctly.
Basically, I have a list of image resources:
val loopImages = listOf(R.drawable.one, R.drawable.two, R.drawable.three, R.drawable.four)
and I want to change the pictures in a loop every 1 second. That was my current attempt that didn't work:
val handler = Handler()
val runnable = Runnable() {
var i = 0
fun run() {
binding.slideImage.setImageResource(loopImages[i])
i++
if (i > loopImages.size - 1) {
i = 0
}
handler.postDelayed({ run() }, 2000)
}
}
handler.postDelayed(runnable, 2000)
Is there any way to do it completely in Kotlin?
private fun startSlider() {
Handler().apply {
val runnable = object : Runnable {
var index = 0
var imageView = findViewById<ImageView>(R.id.imageView)
override fun run() {
imageView.setImageResource(loopImages[index])
index++
if (index > loopImages.size - 1) {
index = 0
}
postDelayed(this, 1000)
}
}
postDelayed(runnable, 1000)
}
}
Is there any way to do it completely in Kotlin?
Not really sure what you mean by this - your code is written in Kotlin syntax.
You can achieve a similar result using coroutines which removes the requirement for runnables and handlers, if that is closer to what you meant.
Untested code but something like this should work :
private fun imageChanger(scope: CoroutineScope, images: List<Int>, target: ImageView, periodMillis : Long = 2_000): Job =
scope.launch(Dispatchers.Main.immediate) {
if (images.isNotEmpty()) {
var idx = 0
while (isActive) {
if(idx >= images.size) idx = 0
images[idx].let(target::setImageResource)
idx++
delay(periodMillis)
}
}
}
Its a bit scrappy but can be refined.
I know this question has been asked quite often here, but non of the answers helped me.
I am writting a gallery app with a thumbes-regeneration feature. In oder to show the progress i added the progressbar which should count the number of created thumbnails. After each finished thumbnail-generation i dispatch a Redux event and listen to it in my Fragement, in order to change the progressbar.
Generating all thumbnails for all visible photos/videos
private fun onMenuRefreshThumbs(activity: Activity) {
val mediaPath = Redux.store.currentState.mediaPath
val fileRepository = FileRepository(context = activity, mediaPath = mediaPath)
activity.runOnUiThread {
fileRepository.regenerateThumbs(activity)
}
}
Functions inside the above used FileRepository:
fun regenerateThumbs(context: Context) {
val success = File(getAbsoluteThumbsDir(context, mediaPath)).deleteRecursively()
getMediaItems()
}
fun getMediaItems(): MediaItemList {
val success = File(thumbPath).mkdirs()
val isThumbsEmpty = File(thumbPath).listFiles().isEmpty()
val mediaFileList = File(mediaPath).listFiles().
.sortedByDescending { it.lastModified() }
val list = MediaItemList()
mediaFileList.apply {
forEach {
list.add(MediaItem(it.name, 0, 0))
if (isThumbsEmpty) {
getOrCreateThumb(it)
Redux.store.dispatch(FileUpdateAction(it))
}
}
}
return list
}
Subscribing to Redux in the Fragement:
private fun subscribeRedux() {
val handler = Handler(Looper.getMainLooper())
val activity = requireActivity()
subscriber = { state: AppState ->
when (state.action) {
...
is ClearSelection -> {
progressCounter = 0
// fragment_gallery_progress.visibility = View.GONE
}
is FileUpdateAction -> {
Handler().post {
progressCounter++
fragment_gallery_progress.visibility = View.VISIBLE
fragment_gallery_progress.progress = progressCounter
// fragment_gallery_progress.invalidate()
log.d("test: Thumb Index $progressCounter ${state.action.mediaItem.name} was created")
}
Unit
}
}
}.apply {
Redux.store.subscribe(this)
}
}
I tried all difference version of calling a thread in both cases. But no matter if its done with the handler or by activity.runOnUiThread, the progressbar never changes untill all thumbs are finished and the progressbar jumps from 0 to the maximum number. I can see the logs which are written in the right time, but not the progressbar changing.
I could fix my problem with following steps:
Removing the runOnUiThread() call
private fun onMenuRefreshThumbs(activity: Activity) {
val mediaPath = Redux.store.currentState.mediaPath
val fileRepository = FileRepository(context = activity, mediaPath = mediaPath)
fileRepository.regenerateThumbs(activity)
}
Adding a thread for each Thumbs-Generation:
fun getMediaItems(): MediaItemList {
val success = File(thumbPath).mkdirs()
val isThumbsEmpty = File(thumbPath).listFiles().isEmpty()
val mediaFileList = File(mediaPath).listFiles().
.sortedByDescending { it.lastModified() }
val list = MediaItemList()
mediaFileList.apply {
forEach {
list.add(MediaItem(it.name, 0, 0))
if (isThumbsEmpty) {
Thread {
getOrCreateThumb(it)
Redux.store.dispatch(FileUpdateAction(it))
}.start()
}
}
...
I have a video of 10 seconds. I want to loop a segment from 2 seconds to 6 seconds. Starting the player at the right time is easy:
player?.seekTo(2000)
I don't think there is a functionality available in the ExoPlayer2 library to define an end position. So I tried to add a delay co-routine. With the method seekToPositionAndStartCounter. This works in a separate project. But used in the actual project, onPlayerStateChanged gets triggered a lot of times all of a sudden.
var elapsedTime = 0L // just for testing
private fun showVideoWhenDoneLoading() {
videoView?.player?.addListener(object : Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
super.onPlayerStateChanged(playWhenReady, playbackState)
if (playbackState == Player.STATE_READY) {
if (playWhenReady) {
seekToPositionAndStartCounter()
} else {
cancelCounter()
}
}
}
})
}
private fun seekToPositionAndStartCounter() {
elapsedTime = System.currentTimeMillis()
cancelCounter()
job = GlobalScope.launch(Dispatchers.IO) {
Timber.d("starting Thread name = ${Thread.currentThread().name}")
player?.seekTo(startPosition)
if (duration != 0L) {
runBlocking {
delay(duration)
}
GlobalScope.launch(Dispatchers.Main) {
Timber.d("duration = $duration elapsedTime = ${System.currentTimeMillis()-elapsedTime} Thread name = ${Thread.currentThread().name}")
seekToPositionAndStartCounter()
}
}
}
}
private fun cancelCounter() {
job?.cancel()
job = null
}
If you don't try this then think about this.
getCurrentPosition of player and if player reach end position which you want then run player.seekto(2) it loops the video in specific segment of video.