From 16602218f9ca9ddf6d52bee07cdcc5ad5730b1d7 Mon Sep 17 00:00:00 2001
From: Sahil Bajaj <sahilbajaj.nc@gmail.com>
Date: Tue, 25 May 2021 16:09:26 +0530
Subject: [PATCH 001/101] dummy commit

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 8aa42823..51191fe2 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
 
 *Like the project, make profit from it, or simply want to thank back? Please consider [sponsoring me](https://github.com/sponsors/natario1)!*
 
-# Transcoder
+## Transcoder
 
 Transcodes and compresses video files into the MP4 format, with audio support, using hardware-accelerated
 Android codecs available on the device. Works on API 18+.
@@ -90,4 +90,4 @@ Transcoder.into(filePath)
         }).transcode()
 ```
 
-Take a look at the demo app for a complete example.
\ No newline at end of file
+Take a look at the demo app for a complete example.

From 91977182d2d47cdd7df3c85c5dd8b81d4b698bb2 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Tue, 25 May 2021 22:06:33 +0530
Subject: [PATCH 002/101] make otalia not internal

---
 .../java/com/otaliastudios/transcoder/internal/Codecs.kt    | 2 +-
 .../com/otaliastudios/transcoder/internal/DataSources.kt    | 2 +-
 .../java/com/otaliastudios/transcoder/internal/Segment.kt   | 2 +-
 .../java/com/otaliastudios/transcoder/internal/Segments.kt  | 2 +-
 .../java/com/otaliastudios/transcoder/internal/Timer.kt     | 2 +-
 .../java/com/otaliastudios/transcoder/internal/Tracks.kt    | 2 +-
 .../otaliastudios/transcoder/internal/audio/AudioEngine.kt  | 2 +-
 .../com/otaliastudios/transcoder/internal/audio/chunks.kt   | 2 +-
 .../transcoder/internal/audio/remix/AudioRemixer.kt         | 2 +-
 .../com/otaliastudios/transcoder/internal/audio/shorts.kt   | 2 +-
 .../com/otaliastudios/transcoder/internal/codec/Decoder.kt  | 6 +++---
 .../transcoder/internal/codec/DecoderDropper.kt             | 2 +-
 .../otaliastudios/transcoder/internal/codec/DecoderTimer.kt | 4 ++--
 .../com/otaliastudios/transcoder/internal/codec/Encoder.kt  | 6 +++---
 .../com/otaliastudios/transcoder/internal/data/Bridge.kt    | 2 +-
 .../com/otaliastudios/transcoder/internal/data/Reader.kt    | 6 +++---
 .../otaliastudios/transcoder/internal/data/ReaderTimer.kt   | 2 +-
 .../com/otaliastudios/transcoder/internal/data/Seeker.kt    | 2 +-
 .../com/otaliastudios/transcoder/internal/data/Writer.kt    | 6 +++---
 .../otaliastudios/transcoder/internal/pipeline/Pipeline.kt  | 2 +-
 .../com/otaliastudios/transcoder/internal/pipeline/State.kt | 2 +-
 .../com/otaliastudios/transcoder/internal/pipeline/Step.kt  | 4 ++--
 .../com/otaliastudios/transcoder/internal/pipeline/steps.kt | 6 +++---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt          | 2 +-
 .../transcoder/internal/thumbnails/ThumbnailsEngine.kt      | 2 +-
 .../transcoder/internal/transcode/DefaultTranscodeEngine.kt | 2 +-
 .../transcoder/internal/transcode/TranscodeEngine.kt        | 2 +-
 .../otaliastudios/transcoder/internal/video/FrameDropper.kt | 2 +-
 .../transcoder/internal/video/VideoPublisher.kt             | 2 +-
 .../transcoder/internal/video/VideoRenderer.kt              | 2 +-
 .../transcoder/internal/video/VideoSnapshots.kt             | 2 +-
 31 files changed, 43 insertions(+), 43 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
index 9a138acb..f5440e87 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
@@ -19,7 +19,7 @@ import com.otaliastudios.transcoder.strategy.TrackStrategy
  * output timestamps, even if input timestamps are. This would later create crashes when passing
  * data to MediaMuxer / MPEG4Writer.
  */
-internal class Codecs(
+class Codecs(
         private val sources: DataSources,
         private val tracks: Tracks,
         private val current: TrackMap<Int>
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
index cb441d7b..225d746b 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
@@ -9,7 +9,7 @@ import com.otaliastudios.transcoder.internal.utils.trackMapOf
 import com.otaliastudios.transcoder.source.BlankAudioDataSource
 import com.otaliastudios.transcoder.source.DataSource
 
-internal class DataSources private constructor(
+class DataSources private constructor(
         videoSources: List<DataSource>,
         audioSources: List<DataSource>,
 ) : TrackMap<List<DataSource>> {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt
index b8884169..807593fe 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt
@@ -5,7 +5,7 @@ import com.otaliastudios.transcoder.internal.pipeline.Pipeline
 import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.internal.utils.Logger
 
-internal class Segment(
+class Segment(
         val type: TrackType,
         val index: Int,
         private val pipeline: Pipeline,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
index bdcc31a1..62d59ebe 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
@@ -8,7 +8,7 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.internal.utils.TrackMap
 import com.otaliastudios.transcoder.internal.utils.mutableTrackMapOf
 
-internal class Segments(
+class Segments(
         private val sources: DataSources,
         private val tracks: Tracks,
         private val factory: (TrackType, Int, TrackStatus, MediaFormat) -> Pipeline
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt
index 9d9b822e..4ce662cc 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt
@@ -6,7 +6,7 @@ import com.otaliastudios.transcoder.internal.utils.TrackMap
 import com.otaliastudios.transcoder.source.DataSource
 import com.otaliastudios.transcoder.time.TimeInterpolator
 
-internal class Timer(
+class Timer(
         private val interpolator: TimeInterpolator,
         private val sources: DataSources,
         private val tracks: Tracks,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt
index bafcc1cd..a099bb68 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt
@@ -10,7 +10,7 @@ import com.otaliastudios.transcoder.internal.utils.trackMapOf
 import com.otaliastudios.transcoder.source.DataSource
 import com.otaliastudios.transcoder.strategy.TrackStrategy
 
-internal class Tracks(
+class Tracks(
         strategies: TrackMap<TrackStrategy>,
         sources: DataSources,
         videoRotation: Int,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
index cfc526aa..d851d9d1 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
@@ -18,7 +18,7 @@ import kotlin.math.floor
  * Performs audio rendering, from decoder output to encoder input, applying sample rate conversion,
  * remixing, stretching. TODO: With some extra work this could be split in different steps.
  */
-internal class AudioEngine(
+class AudioEngine(
         private val stretcher: AudioStretcher,
         private val resampler: AudioResampler,
         private val targetFormat: MediaFormat
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt
index 52eaa357..56f28ea1 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt
@@ -19,7 +19,7 @@ private data class Chunk(
  * big enough to contain the full processed size, in which case we want to consume only
  * part of the input buffer and keep it available for the next cycle.
  */
-internal class ChunkQueue(private val sampleRate: Int, private val channels: Int) {
+class ChunkQueue(private val sampleRate: Int, private val channels: Int) {
     private val queue = ArrayDeque<Chunk>()
 
     fun isEmpty() = queue.isEmpty()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/remix/AudioRemixer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/remix/AudioRemixer.kt
index fac8b7cd..40b60845 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/remix/AudioRemixer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/remix/AudioRemixer.kt
@@ -6,7 +6,7 @@ import java.nio.ShortBuffer
  * Remixes audio data. See [DownMixAudioRemixer], [UpMixAudioRemixer] or [PassThroughAudioRemixer]
  * for concrete implementations.
  */
-internal interface AudioRemixer {
+interface AudioRemixer {
 
     /**
      * Remixes input audio from input buffer into the output buffer.
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/shorts.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/shorts.kt
index 80959159..56ece6c2 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/shorts.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/shorts.kt
@@ -6,7 +6,7 @@ import java.nio.ShortBuffer
 
 internal const val BYTES_PER_SHORT = 2
 
-internal class ShortBuffers {
+class ShortBuffers {
     private val map = mutableMapOf<String, ShortBuffer>()
 
     fun acquire(name: String, size: Int): ShortBuffer {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index fba72dd9..ce635842 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -19,18 +19,18 @@ import kotlin.properties.Delegates
 import kotlin.properties.Delegates.observable
 
 
-internal open class DecoderData(
+open class DecoderData(
         val buffer: ByteBuffer,
         val timeUs: Long,
         val release: (render: Boolean) -> Unit
 )
 
-internal interface DecoderChannel : Channel {
+interface DecoderChannel : Channel {
     fun handleSourceFormat(sourceFormat: MediaFormat): Surface?
     fun handleRawFormat(rawFormat: MediaFormat)
 }
 
-internal class Decoder(
+class Decoder(
         private val format: MediaFormat, // source.getTrackFormat(track)
         continuous: Boolean, // relevant if the source sends no-render chunks. should we compensate or not?
 ) : QueuedStep<ReaderData, ReaderChannel, DecoderData, DecoderChannel>(), ReaderChannel {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
index 9809b2a4..4b5a38d0 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
@@ -21,7 +21,7 @@ import com.otaliastudios.transcoder.source.DataSource
  * is screwed. Also, if the source jumps forward using seek, we won't catch the jump. This class
  * catches discontinuities only through changes in the render boolean passed to [input].
  */
-internal class DecoderDropper(private val continuous: Boolean) {
+class DecoderDropper(private val continuous: Boolean) {
 
     private val log = Logger("DecoderDropper")
     private val closedDeltas = mutableMapOf<LongRange, Long>()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderTimer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderTimer.kt
index 4c9459a6..ff3cb777 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderTimer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderTimer.kt
@@ -6,7 +6,7 @@ import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.time.TimeInterpolator
 import java.nio.ByteBuffer
 
-internal class DecoderTimerData(
+class DecoderTimerData(
         buffer: ByteBuffer,
         val rawTimeUs: Long,
         timeUs: Long,
@@ -14,7 +14,7 @@ internal class DecoderTimerData(
         release: (render: Boolean) -> Unit
 ) : DecoderData(buffer, timeUs, release)
 
-internal class DecoderTimer(
+class DecoderTimer(
         private val track: TrackType,
         private val interpolator: TimeInterpolator,
 ) : DataStep<DecoderData, DecoderChannel>() {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt
index 730c5633..274b07bd 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt
@@ -19,7 +19,7 @@ import java.util.concurrent.atomic.AtomicInteger
 import kotlin.properties.Delegates
 import kotlin.properties.Delegates.observable
 
-internal data class EncoderData(
+data class EncoderData(
         val buffer: ByteBuffer?, // If present, it must have correct position/remaining!
         val id: Int,
         val timeUs: Long
@@ -27,12 +27,12 @@ internal data class EncoderData(
     companion object { val Empty = EncoderData(null, 0, 0L) }
 }
 
-internal interface EncoderChannel : Channel {
+interface EncoderChannel : Channel {
     val surface: Surface?
     fun buffer(): Pair<ByteBuffer, Int>?
 }
 
-internal class Encoder(
+class Encoder(
         private val codec: MediaCodec,
         override val surface: Surface?,
         ownsCodecStart: Boolean,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Bridge.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Bridge.kt
index 5a75f657..e2c3b6e4 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Bridge.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Bridge.kt
@@ -8,7 +8,7 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
 
-internal class Bridge(private val format: MediaFormat)
+class Bridge(private val format: MediaFormat)
     : Step<ReaderData, ReaderChannel, WriterData, WriterChannel>, ReaderChannel {
 
     private val log = Logger("Bridge")
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt
index 020149a5..8e26fdac 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt
@@ -9,13 +9,13 @@ import com.otaliastudios.transcoder.source.DataSource
 import java.nio.ByteBuffer
 
 
-internal data class ReaderData(val chunk: DataSource.Chunk, val id: Int)
+data class ReaderData(val chunk: DataSource.Chunk, val id: Int)
 
-internal interface ReaderChannel : Channel {
+interface ReaderChannel : Channel {
     fun buffer(): Pair<ByteBuffer, Int>?
 }
 
-internal class Reader(
+class Reader(
         private val source: DataSource,
         private val track: TrackType
 ) : BaseStep<Unit, Channel, ReaderData, ReaderChannel>() {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/ReaderTimer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/ReaderTimer.kt
index 57a48562..8a7c5511 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/ReaderTimer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/ReaderTimer.kt
@@ -5,7 +5,7 @@ import com.otaliastudios.transcoder.internal.pipeline.DataStep
 import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.time.TimeInterpolator
 
-internal class ReaderTimer(
+class ReaderTimer(
         private val track: TrackType,
         private val interpolator: TimeInterpolator
 ) : DataStep<ReaderData, ReaderChannel>() {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
index 610f5647..4cf8c51e 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
@@ -8,7 +8,7 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.source.DataSource
 import java.nio.ByteBuffer
 
-internal class Seeker(
+class Seeker(
         private val source: DataSource,
         positions: List<Long>,
         private val seek: (Long) -> Boolean
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
index 1d8fc899..182d1679 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
@@ -10,18 +10,18 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.sink.DataSink
 import java.nio.ByteBuffer
 
-internal data class WriterData(
+data class WriterData(
         val buffer: ByteBuffer,
         val timeUs: Long,
         val flags: Int,
         val release: () -> Unit
 )
 
-internal interface WriterChannel : Channel {
+interface WriterChannel : Channel {
     fun handleFormat(format: MediaFormat)
 }
 
-internal class Writer(
+class Writer(
         private val sink: DataSink,
         private val track: TrackType
 ) : Step<WriterData, WriterChannel, Unit, Channel>, WriterChannel {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
index d64a1016..183976f7 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
@@ -5,7 +5,7 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 
 private typealias AnyStep = Step<Any, Channel, Any, Channel>
 
-internal class Pipeline private constructor(name: String, private val chain: List<AnyStep>) {
+class Pipeline private constructor(name: String, private val chain: List<AnyStep>) {
 
     private val log = Logger("Pipeline($name)")
     private var headState: State.Ok<Any> = State.Ok(Unit)
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/State.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/State.kt
index 4fb77c79..bdc81726 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/State.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/State.kt
@@ -1,6 +1,6 @@
 package com.otaliastudios.transcoder.internal.pipeline
 
-internal sealed class State<out T> {
+sealed class State<out T> {
 
     // Running
     open class Ok<T>(val value: T) : State<T>() {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt
index 7159182b..6b7385d6 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt
@@ -1,11 +1,11 @@
 package com.otaliastudios.transcoder.internal.pipeline
 
 // TODO this could be Any
-internal interface Channel {
+interface Channel {
     companion object : Channel
 }
 
-internal interface Step<
+interface Step<
         Input: Any,
         InputChannel: Channel,
         Output: Any,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/steps.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/steps.kt
index 9afe4ef7..42118528 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/steps.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/steps.kt
@@ -1,6 +1,6 @@
 package com.otaliastudios.transcoder.internal.pipeline
 
-internal abstract class BaseStep<
+abstract class BaseStep<
         Input: Any,
         InputChannel: Channel,
         Output: Any,
@@ -14,14 +14,14 @@ internal abstract class BaseStep<
     }
 }
 
-internal abstract class DataStep<D: Any, C: Channel> : Step<D, C, D, C> {
+abstract class DataStep<D: Any, C: Channel> : Step<D, C, D, C> {
     override lateinit var channel: C
     override fun initialize(next: C) {
         channel = next
     }
 }
 
-internal abstract class QueuedStep<
+abstract class QueuedStep<
         Input: Any,
         InputChannel: Channel,
         Output: Any,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 7560fa37..21290942 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -36,7 +36,7 @@ import java.nio.ByteBuffer
 import java.nio.ByteOrder
 import kotlin.math.abs
 
-internal class DefaultThumbnailsEngine(
+class DefaultThumbnailsEngine(
         private val dataSources: DataSources,
         private val rotation: Int,
         resizer: Resizer,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index 26eb75a7..7ba13e4d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -8,7 +8,7 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.internal.utils.trackMapOf
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
 
-internal abstract class ThumbnailsEngine {
+abstract class ThumbnailsEngine {
 
     abstract fun thumbnails(progress: (Thumbnail) -> Unit)
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
index 985d7ff4..487ba8e1 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
@@ -24,7 +24,7 @@ import com.otaliastudios.transcoder.stretch.AudioStretcher
 import com.otaliastudios.transcoder.time.TimeInterpolator
 import com.otaliastudios.transcoder.validator.Validator
 
-internal class DefaultTranscodeEngine(
+class DefaultTranscodeEngine(
         private val dataSources: DataSources,
         private val dataSink: DataSink,
         strategies: TrackMap<TrackStrategy>,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
index 34a6610b..a04c5e35 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
@@ -6,7 +6,7 @@ import com.otaliastudios.transcoder.internal.DataSources
 import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.internal.utils.trackMapOf
 
-internal abstract class TranscodeEngine {
+abstract class TranscodeEngine {
 
     abstract fun validate(): Boolean
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt
index 53087887..fea36f62 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt
@@ -2,7 +2,7 @@ package com.otaliastudios.transcoder.internal.video
 
 import com.otaliastudios.transcoder.internal.utils.Logger
 
-internal interface FrameDropper {
+interface FrameDropper {
     fun shouldRender(timeUs: Long): Boolean
 }
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoPublisher.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoPublisher.kt
index 1e9679bd..da1857be 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoPublisher.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoPublisher.kt
@@ -10,7 +10,7 @@ import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.internal.pipeline.Step
 
 
-internal class VideoPublisher: Step<Long, Channel, EncoderData, EncoderChannel> {
+class VideoPublisher: Step<Long, Channel, EncoderData, EncoderChannel> {
 
     override val channel = Channel
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
index cf470a66..d782db80 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
@@ -12,7 +12,7 @@ import com.otaliastudios.transcoder.internal.pipeline.Step
 import com.otaliastudios.transcoder.internal.utils.Logger
 
 
-internal class VideoRenderer(
+class VideoRenderer(
         private val sourceRotation: Int, // intrinsic source rotation
         private val extraRotation: Int, // any extra rotation in TranscoderOptions
         private val targetFormat: MediaFormat,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt
index e8ba3e35..e3d45ae0 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt
@@ -19,7 +19,7 @@ import java.nio.ByteBuffer
 import java.nio.ByteOrder
 import kotlin.math.abs
 
-internal class VideoSnapshots(
+class VideoSnapshots(
         format: MediaFormat,
         requests: List<Long>,
         private val accuracyUs: Long,

From 5ff6436b005caa0dae9b29592e49f4127209128b Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Thu, 27 May 2021 16:35:54 +0530
Subject: [PATCH 003/101] add context as parameter to flow and renderer and
 driver steps

---
 lib/build.gradle.kts                          |   4 +
 .../otaliastudios/transcoder/Thumbnailer.kt   |   7 +-
 .../transcoder/ThumbnailerOptions.kt          |   4 +-
 .../otaliastudios/transcoder/Transcoder.java  |  13 +-
 .../transcoder/TranscoderOptions.java         |   4 +-
 .../transcoder/custom/OtaliaDriver.kt         |  42 ++++++
 .../transcoder/custom/OtaliaExporterNew.kt    |  40 ++++++
 .../otaliastudios/transcoder/custom/RKorge.kt | 124 ++++++++++++++++++
 .../transcoder/internal/Codecs.kt             |  16 ++-
 .../transcoder/internal/Segments.kt           |  13 +-
 .../transcoder/internal/pipeline/Pipeline.kt  |   7 +-
 .../transcoder/internal/pipeline/pipelines.kt |  33 ++++-
 .../thumbnails/DefaultThumbnailsEngine.kt     |  14 +-
 .../internal/thumbnails/ThumbnailsEngine.kt   |   7 +-
 .../transcode/DefaultTranscodeEngine.kt       |  20 +--
 .../internal/transcode/TranscodeEngine.kt     |   7 +-
 16 files changed, 307 insertions(+), 48 deletions(-)
 create mode 100644 lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaDriver.kt
 create mode 100644 lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaExporterNew.kt
 create mode 100644 lib/src/main/java/com/otaliastudios/transcoder/custom/RKorge.kt

diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index a670c667..52561f7d 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -30,6 +30,10 @@ dependencies {
     androidTestImplementation("androidx.test:rules:1.3.0")
     androidTestImplementation("androidx.test.ext:junit:1.1.2")
     androidTestImplementation("org.mockito:mockito-android:2.28.2")
+    implementation(project(":shared:features:avcore"))
+    implementation(project(":shared:features:windowsystem"))
+    api(project(":shared:renderer"))
+    implementation(Korlibs.korge)
 }
 
 publisher {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt b/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt
index c519e49a..1f70b655 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt
@@ -1,5 +1,6 @@
 package com.otaliastudios.transcoder
 
+import android.content.Context
 import com.otaliastudios.transcoder.internal.thumbnails.ThumbnailsEngine
 import com.otaliastudios.transcoder.internal.utils.ThreadPool
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
@@ -8,14 +9,14 @@ import java.util.concurrent.Future
 
 class Thumbnailer private constructor() {
 
-    fun thumbnails(options: ThumbnailerOptions): Future<Void> {
+    fun thumbnails(context: Context,options: ThumbnailerOptions): Future<Void> {
         return ThreadPool.executor.submit(Callable {
-            ThumbnailsEngine.thumbnails(options)
+            ThumbnailsEngine.thumbnails(context,options)
             null
         })
     }
 
-    fun thumbnails(builder: ThumbnailerOptions.Builder.() -> Unit) = thumbnails(
+    fun thumbnails(context: Context,builder: ThumbnailerOptions.Builder.() -> Unit) = thumbnails(context,
             options = ThumbnailerOptions.Builder().apply(builder).build()
     )
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
index 5a0a35d6..14953331 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
@@ -104,8 +104,8 @@ class ThumbnailerOptions(
             )
         }
 
-        fun thumbnails(): Future<Void> {
-            return Thumbnailer.getInstance().thumbnails(build())
+        fun thumbnails(context: Context): Future<Void> {
+            return Thumbnailer.getInstance().thumbnails(context,build())
         }
     }
 }
\ No newline at end of file
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java b/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java
index 12e32dd1..afbc305a 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java
@@ -15,6 +15,7 @@
  */
 package com.otaliastudios.transcoder;
 
+import android.content.Context;
 import android.os.Build;
 
 import com.otaliastudios.transcoder.internal.transcode.TranscodeEngine;
@@ -23,7 +24,6 @@
 import com.otaliastudios.transcoder.validator.Validator;
 
 import java.io.FileDescriptor;
-import java.util.concurrent.Callable;
 import java.util.concurrent.Future;
 
 import androidx.annotation.NonNull;
@@ -95,13 +95,10 @@ public static TranscoderOptions.Builder into(@NonNull DataSink dataSink) {
      * @return a Future that completes when transcoding is completed
      */
     @NonNull
-    public Future<Void> transcode(@NonNull final TranscoderOptions options) {
-        return ThreadPool.getExecutor().submit(new Callable<Void>() {
-            @Override
-            public Void call() throws Exception {
-                TranscodeEngine.transcode(options);
-                return null;
-            }
+    public Future<Void> transcode(Context context, @NonNull final TranscoderOptions options) {
+        return ThreadPool.getExecutor().submit(() -> {
+            TranscodeEngine.transcode(context, options);
+            return null;
         });
     }
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
index 42b35679..24369255 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
@@ -386,8 +386,8 @@ public TranscoderOptions build() {
         }
 
         @NonNull
-        public Future<Void> transcode() {
-            return Transcoder.getInstance().transcode(build());
+        public Future<Void> transcode(Context context) {
+            return Transcoder.getInstance().transcode(context, build());
         }
     }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaDriver.kt b/lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaDriver.kt
new file mode 100644
index 00000000..21a5c163
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaDriver.kt
@@ -0,0 +1,42 @@
+package com.otaliastudios.transcoder.custom
+
+import android.graphics.Point
+import com.otaliastudios.transcoder.internal.pipeline.BaseStep
+import com.otaliastudios.transcoder.internal.pipeline.Channel
+import com.otaliastudios.transcoder.internal.pipeline.State
+import com.otaliastudios.transcoder.internal.utils.Logger
+import io.invideo.features.avcore.VideoConfig
+import kotlin.time.ExperimentalTime
+
+@OptIn(ExperimentalTime::class)
+class OtaliaDriver(private val videoConfig: VideoConfig) :
+    BaseStep<Unit, Channel, RenderingData, RenderingChannel>() {
+
+    override val channel = Channel
+    private val log = Logger("Driver")
+
+    private var timestampMs: Long = 0
+    private var prevTimestampMs: Long = 0
+
+    private var frameCount: Int = 0
+
+    override fun initialize(next: RenderingChannel) {
+        super.initialize(next)
+        next.setDimensions(Point(videoConfig.width, videoConfig.height))
+    }
+
+    override fun step(state: State.Ok<Unit>, fresh: Boolean): State<RenderingData> {
+        return if (timestampMs > videoConfig.duration) {
+            log.i("Source is drained! Returning Eos as soon as possible.")
+            State.Eos(RenderingData(timestampMs, prevTimestampMs) {})
+        } else {
+            val data = RenderingData(timestampMs, prevTimestampMs) {
+                prevTimestampMs = timestampMs
+                frameCount++
+                timestampMs = frameCount * 1000L / videoConfig.fps
+            }
+            return State.Ok(data)
+        }
+
+    }
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaExporterNew.kt b/lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaExporterNew.kt
new file mode 100644
index 00000000..8c34d16c
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaExporterNew.kt
@@ -0,0 +1,40 @@
+package com.otaliastudios.transcoder.custom
+
+import android.content.Context
+import com.otaliastudios.transcoder.Transcoder
+import com.otaliastudios.transcoder.TranscoderListener
+import com.otaliastudios.transcoder.resize.PassThroughResizer
+import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
+import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy
+
+class OtaliaExporterNew {
+
+    fun start(context: Context) {
+        val builder = Transcoder.into("/sdcard/transcoder.mp4")
+        builder.setListener(object : TranscoderListener {
+            override fun onTranscodeProgress(progress: Double) {
+            }
+
+            override fun onTranscodeCompleted(successCode: Int) {
+            }
+
+            override fun onTranscodeCanceled() {
+            }
+
+            override fun onTranscodeFailed(exception: Throwable) {
+            }
+        })
+            .addDataSource("/sdcard/big_bunny.mp4")// necessary to add but not used
+            .setAudioTrackStrategy(RemoveTrackStrategy())
+            .setVideoTrackStrategy(
+                DefaultVideoStrategy.Builder()
+                    .addResizer(PassThroughResizer())
+                    .frameRate(30)
+                    .build()
+            )
+            .build()
+        builder.transcode(context)
+
+
+    }
+}
\ No newline at end of file
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/custom/RKorge.kt b/lib/src/main/java/com/otaliastudios/transcoder/custom/RKorge.kt
new file mode 100644
index 00000000..2586006e
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/custom/RKorge.kt
@@ -0,0 +1,124 @@
+package com.otaliastudios.transcoder.custom
+
+import android.content.Context
+import android.graphics.Point
+import com.otaliastudios.transcoder.internal.pipeline.Channel
+import com.otaliastudios.transcoder.internal.pipeline.State
+import com.otaliastudios.transcoder.internal.pipeline.Step
+import io.invideo.renderer.IVRootGameMainAndroid
+import io.invideo.renderer.RenderTreeEvent
+import io.invideo.renderer.RendererConfiguration
+import io.invideo.renderer.SceneTimeUpdateEvent
+import io.invideo.renderer.UpdateResourceEvent
+import io.invideo.renderer.VideoPlayerFactoryInterface
+import io.invideo.renderer.VideoSeekEvent
+import io.invideo.renderer.extensions.dispatchRenderTreeEvent
+import io.invideo.renderer.extensions.dispatchSceneTimeUpdateEvent
+import io.invideo.renderer.extensions.dispatchUpdateResourcesEvent
+import io.invideo.renderer.extensions.dispatchVideoSeekEvent
+import io.invideo.renderer.getTimeline
+import io.invideo.renderer.rendertree.Node
+import io.invideo.renderer.rendertree.RenderTree
+import io.invideo.renderer.timeline.IVEventDispatcher
+import io.invideo.renderer.timeline.TimeLinePlayer
+import io.invideo.renderer.timeline.VideoEventDispatcher
+import kotlin.time.ExperimentalTime
+
+data class RenderingData(
+    val timeMsCurrent: Long,
+    val timeMsPrevious: Long,
+    val updateTime: () -> Unit
+)
+
+interface RenderingChannel : Channel {
+    fun setDimensions(point: Point)
+}
+
+@OptIn(ExperimentalTime::class)
+class RKorge(
+    context: Context,
+    config: RendererConfiguration,
+    videoPlayerFactoryInterface: VideoPlayerFactoryInterface?
+) : Step<RenderingData, RenderingChannel, Long, Channel>, RenderingChannel {
+    private var isInitialized = false
+    var reshape = true
+
+    val rootGameMain = IVRootGameMainAndroid(context, config, videoPlayerFactoryInterface)
+    private var oldBoundSize = Point(0, 0)
+
+    private val timelinePlayer: TimeLinePlayer
+    private val sceneTimeUpdateEvent = SceneTimeUpdateEvent(timeInSeconds = 0.0)
+    lateinit var size: Point
+    override val channel = this
+
+    init {
+        timelinePlayer = TimeLinePlayer(getTimeline(), EventDispatcher())
+    }
+
+    override fun step(state: State.Ok<RenderingData>, fresh: Boolean): State<Long> {
+        val (timeMsCurrent, timeMsPrevious) = state.value
+
+        return if (state is State.Eos) {
+            State.Eos(0L)
+        } else {
+            sceneTimeUpdateEvent.timeInSeconds = timeMsCurrent / 1000.0
+            rootGameMain.gameWindow.dispatchSceneTimeUpdateEvent(sceneTimeUpdateEvent)
+
+            timelinePlayer.checkAndUpdate(timeMsPrevious / 1000.0, timeMsCurrent / 1000.0, false)
+            render(size, 1.0f)
+            state.value.updateTime()
+            State.Ok(state.value.timeMsCurrent * 1000L)
+        }
+
+    }
+
+    override fun setDimensions(point: Point) {
+        size = point
+    }
+
+    fun render(boundSize: Point, contentScaleFactor: Float = 1.0f) {
+        if (!isInitialized) {
+            isInitialized = true
+            rootGameMain.preRunMain()
+            rootGameMain.gameWindow.dispatchInitEvent()
+            rootGameMain.runMain()
+            reshape = true
+        }
+
+        if (!reshape && oldBoundSize.equals(boundSize.x, boundSize.y)) {
+            reshape = true
+        }
+        oldBoundSize = boundSize
+
+        if (reshape) {
+            reshape = false
+            val width = (boundSize.x * contentScaleFactor).toInt()
+            val height = (boundSize.y * contentScaleFactor).toInt()
+            rootGameMain.gameWindow.dispatchReshapeEvent(0, 0, width, height)
+        }
+
+        rootGameMain.gameWindow.frame()
+    }
+
+    inner class EventDispatcher : IVEventDispatcher {
+        override fun updateResources(node: Node, shouldLoad: Boolean) {
+            val event = UpdateResourceEvent(node, shouldLoad)
+            rootGameMain.gameWindow.dispatchUpdateResourcesEvent(event)
+        }
+
+        override fun updateRenderTree(renderTree: RenderTree) {
+            val event = RenderTreeEvent(renderTree)
+            rootGameMain.gameWindow.dispatchRenderTreeEvent(event)
+        }
+
+        override fun play() {
+        }
+
+        override fun pause() {
+        }
+
+        override fun seek(seekTimeMs: Long) {
+            rootGameMain.gameWindow.dispatchVideoSeekEvent(VideoSeekEvent(seekTimeMs))
+        }
+    }
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
index f5440e87..e9bb7f55 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
@@ -1,7 +1,10 @@
 package com.otaliastudios.transcoder.internal
 
 import android.media.MediaCodec
+import android.media.MediaCodecInfo
 import android.media.MediaFormat
+import android.os.Build
+import android.util.Log
 import android.view.Surface
 import com.otaliastudios.transcoder.common.TrackStatus
 import com.otaliastudios.transcoder.common.TrackType
@@ -39,7 +42,18 @@ class Codecs(
         }
 
         private val lazyVideo by lazy {
-            val format = tracks.outputFormats.video
+//            val format = tracks.outputFormats.video
+            val format = MediaFormat.createVideoFormat("video/avc", 720, 720)
+            format.setInteger(
+                MediaFormat.KEY_COLOR_FORMAT,
+                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
+            )
+            format.setInteger(MediaFormat.KEY_BIT_RATE, 10 * 1024 * 1024 /*calcBitRate()*/)
+            format.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
+            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
+            format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 720 * 720)
+
+
             val codec = MediaCodec.createEncoderByType(format.getString(MediaFormat.KEY_MIME)!!)
             codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
             codec to codec.createInputSurface()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
index 62d59ebe..272bb161 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
@@ -1,17 +1,17 @@
 package com.otaliastudios.transcoder.internal
 
+import android.content.Context
 import android.media.MediaFormat
 import com.otaliastudios.transcoder.common.TrackStatus
 import com.otaliastudios.transcoder.common.TrackType
 import com.otaliastudios.transcoder.internal.pipeline.Pipeline
 import com.otaliastudios.transcoder.internal.utils.Logger
-import com.otaliastudios.transcoder.internal.utils.TrackMap
 import com.otaliastudios.transcoder.internal.utils.mutableTrackMapOf
 
 class Segments(
         private val sources: DataSources,
         private val tracks: Tracks,
-        private val factory: (TrackType, Int, TrackStatus, MediaFormat) -> Pipeline
+        private val factory: (Context, TrackType, Int, TrackStatus, MediaFormat) -> Pipeline
 ) {
 
     private val log = Logger("Segments")
@@ -34,7 +34,7 @@ class Segments(
      * - data sources don't have this type
      * - transcoding for this track is over
      */
-    fun next(type: TrackType): Segment? {
+    fun next(context: Context,type: TrackType): Segment? {
         val currentIndex = currentIndex[type]
         val requestedIndex = requestedIndex[type]
         return when {
@@ -43,7 +43,7 @@ class Segments(
             }
             // We need to open a new segment, if possible.
             // createSegment will make requested == current.
-            requestedIndex > currentIndex -> tryCreateSegment(type, requestedIndex)
+            requestedIndex > currentIndex -> tryCreateSegment(context, type, requestedIndex)
             // requested == current
             current[type].canAdvance() -> current[type]
             // requested == current, but this segment is finished:
@@ -51,7 +51,7 @@ class Segments(
             // so if this is the last one, we'll return null.
             else -> {
                 destroySegment(current[type])
-                next(type)
+                next(context,type)
             }
         }
     }
@@ -61,7 +61,7 @@ class Segments(
         current.audioOrNull()?.let { destroySegment(it) }
     }
 
-    private fun tryCreateSegment(type: TrackType, index: Int): Segment? {
+    private fun tryCreateSegment(context: Context,type: TrackType, index: Int): Segment? {
         // Return null if out of bounds, either because segments are over or because the
         // source set does not have sources for this track type.
         val source = sources[type].getOrNull(index) ?: return null
@@ -85,6 +85,7 @@ class Segments(
         // who check it during pipeline init.
         currentIndex[type] = index
         val pipeline = factory(
+            context,
                 type,
                 index,
                 tracks.all[type],
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
index 183976f7..b5a64453 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
@@ -48,8 +48,7 @@ class Pipeline private constructor(name: String, private val chain: List<AnyStep
     }
 
     private fun executeStep(previous: State.Ok<Any>, step: AnyStep, fresh: Boolean): State.Ok<Any>? {
-        val state = step.step(previous, fresh)
-        return when (state) {
+        return when (val state = step.step(previous, fresh)) {
             is State.Ok -> state
             is State.Retry -> executeStep(previous, step, fresh = false)
             is State.Wait -> null
@@ -58,7 +57,7 @@ class Pipeline private constructor(name: String, private val chain: List<AnyStep
 
     companion object {
         @Suppress("UNCHECKED_CAST")
-        internal fun build(name: String, builder: () -> Builder<*, Channel> = { Builder<Unit, Channel>() }): Pipeline {
+         fun build(name: String, builder: () -> Builder<*, Channel> = { Builder<Unit, Channel>() }): Pipeline {
             return Pipeline(name, builder().steps as List<AnyStep>)
         }
     }
@@ -72,7 +71,7 @@ class Pipeline private constructor(name: String, private val chain: List<AnyStep
     }
 }
 
-internal operator fun <
+operator fun <
         CurrData: Any, CurrChannel: Channel,
         NewData: Any, NewChannel: Channel
 > Step<Unit, Channel, CurrData, CurrChannel>.plus(
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
index e0a4b33e..e89f0688 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
@@ -1,6 +1,8 @@
 package com.otaliastudios.transcoder.internal.pipeline
 
+import android.content.Context
 import android.media.MediaFormat
+import android.util.Log
 import com.otaliastudios.transcoder.common.TrackType
 import com.otaliastudios.transcoder.internal.Codecs
 import com.otaliastudios.transcoder.internal.audio.AudioEngine
@@ -18,6 +20,12 @@ import com.otaliastudios.transcoder.sink.DataSink
 import com.otaliastudios.transcoder.source.DataSource
 import com.otaliastudios.transcoder.stretch.AudioStretcher
 import com.otaliastudios.transcoder.time.TimeInterpolator
+import io.invideo.features.avcore.VideoConfig
+import com.otaliastudios.transcoder.custom.OtaliaDriver
+import com.otaliastudios.transcoder.custom.RKorge
+import io.invideo.renderer.RenderTarget
+import io.invideo.renderer.RendererConfiguration
+import io.invideo.renderer.rendertree.Size
 
 internal fun EmptyPipeline() = Pipeline.build("Empty")
 
@@ -34,6 +42,7 @@ internal fun PassThroughPipeline(
 }
 
 internal fun RegularPipeline(
+        context: Context,
         track: TrackType,
         source: DataSource,
         sink: DataSink,
@@ -44,10 +53,32 @@ internal fun RegularPipeline(
         audioStretcher: AudioStretcher,
         audioResampler: AudioResampler
 ) = when (track) {
-    TrackType.VIDEO -> VideoPipeline(source, sink, interpolator, format, codecs, videoRotation)
+    TrackType.VIDEO -> KorgeVideoPipeline(context,source, sink, interpolator, format, codecs, videoRotation)
     TrackType.AUDIO -> AudioPipeline(source, sink, interpolator, format, codecs, audioStretcher, audioResampler)
 }
 
+private fun KorgeVideoPipeline(
+        context: Context,
+        source: DataSource,
+        sink: DataSink,
+        interpolator: TimeInterpolator,
+        format: MediaFormat,
+        codecs: Codecs,
+        videoRotation: Int
+) = Pipeline.build("Video") {
+
+        // Driver -> Renderer ->
+        OtaliaDriver(VideoConfig(720, 720, 10 * 1024 * 1024, duration = 20000, 30)) +
+                RKorge(context, RendererConfiguration(
+                        Size(720.0, 720.0),
+                        RenderTarget.DISPLAY
+                ) { Log.d("Pipeline", "Renderer Initialization complete") }, null
+                ) +
+                VideoPublisher() +
+                Encoder(codecs, TrackType.VIDEO) +
+                Writer(sink, TrackType.VIDEO)
+}
+
 private fun VideoPipeline(
         source: DataSource,
         sink: DataSink,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 21290942..664467c2 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -1,5 +1,6 @@
 package com.otaliastudios.transcoder.internal.thumbnails
 
+import android.content.Context
 import android.graphics.Bitmap
 import android.media.MediaFormat
 import android.media.MediaFormat.KEY_HEIGHT
@@ -76,10 +77,11 @@ class DefaultThumbnailsEngine(
     }
 
     private fun createPipeline(
-            type: TrackType,
-            index: Int,
-            status: TrackStatus,
-            outputFormat: MediaFormat
+        context: Context,
+        type: TrackType,
+        index: Int,
+        status: TrackStatus,
+        outputFormat: MediaFormat
     ): Pipeline {
         log.i("Creating pipeline #$index. absoluteUs=${positions.joinToString { it.first.toString() }}")
         val stubs = positions.mapNotNull { (positionUs, request) ->
@@ -113,10 +115,10 @@ class DefaultThumbnailsEngine(
 
     private lateinit var progress: (Thumbnail) -> Unit
 
-    override fun thumbnails(progress: (Thumbnail) -> Unit) {
+    override fun thumbnails(context: Context,progress: (Thumbnail) -> Unit) {
         this.progress = progress
         while (true) {
-            val advanced = segments.next(TrackType.VIDEO)?.advance() ?: false
+            val advanced = segments.next(context,TrackType.VIDEO)?.advance() ?: false
             val completed = !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
             if (Thread.interrupted()) {
                 throw InterruptedException()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index 7ba13e4d..7fc312d7 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -1,5 +1,6 @@
 package com.otaliastudios.transcoder.internal.thumbnails
 
+import android.content.Context
 import com.otaliastudios.transcoder.ThumbnailerOptions
 import com.otaliastudios.transcoder.Transcoder
 import com.otaliastudios.transcoder.TranscoderOptions
@@ -10,7 +11,7 @@ import com.otaliastudios.transcoder.thumbnail.Thumbnail
 
 abstract class ThumbnailsEngine {
 
-    abstract fun thumbnails(progress: (Thumbnail) -> Unit)
+    abstract fun thumbnails(context: Context,progress: (Thumbnail) -> Unit)
 
     abstract fun cleanup()
 
@@ -24,7 +25,7 @@ abstract class ThumbnailsEngine {
         }
 
         @JvmStatic
-        fun thumbnails(options: ThumbnailerOptions) {
+        fun thumbnails(context: Context,options: ThumbnailerOptions) {
             log.i("thumbnails(): called...")
             var engine: ThumbnailsEngine? = null
             val dispatcher = ThumbnailsDispatcher(options)
@@ -35,7 +36,7 @@ abstract class ThumbnailsEngine {
                         resizer = options.resizer,
                         requests = options.thumbnailRequests
                 )
-                engine.thumbnails {
+                engine.thumbnails(context) {
                     dispatcher.dispatchThumbnail(it)
                 }
                 dispatcher.dispatchCompletion()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
index 487ba8e1..7268493f 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
@@ -1,11 +1,12 @@
 package com.otaliastudios.transcoder.internal.transcode
 
+import android.content.Context
 import android.media.MediaFormat
 import com.otaliastudios.transcoder.common.TrackStatus
 import com.otaliastudios.transcoder.common.TrackType
-import com.otaliastudios.transcoder.internal.*
 import com.otaliastudios.transcoder.internal.Codecs
 import com.otaliastudios.transcoder.internal.DataSources
+import com.otaliastudios.transcoder.internal.Segment
 import com.otaliastudios.transcoder.internal.Segments
 import com.otaliastudios.transcoder.internal.Timer
 import com.otaliastudios.transcoder.internal.Tracks
@@ -61,10 +62,11 @@ class DefaultTranscodeEngine(
     }
 
     private fun createPipeline(
-            type: TrackType,
-            index: Int,
-            status: TrackStatus,
-            outputFormat: MediaFormat
+        context: Context,
+        type: TrackType,
+        index: Int,
+        status: TrackStatus,
+        outputFormat: MediaFormat
     ): Pipeline {
         log.w("createPipeline($type, $index, $status), format=$outputFormat")
         val interpolator = timer.interpolator(type, index)
@@ -79,7 +81,7 @@ class DefaultTranscodeEngine(
             TrackStatus.ABSENT -> EmptyPipeline()
             TrackStatus.REMOVING -> EmptyPipeline()
             TrackStatus.PASS_THROUGH -> PassThroughPipeline(type, source, sink, interpolator)
-            TrackStatus.COMPRESSING -> RegularPipeline(type,
+            TrackStatus.COMPRESSING -> RegularPipeline(context,type,
                     source, sink, interpolator, outputFormat, codecs,
                     videoRotation, audioStretcher, audioResampler)
         }
@@ -98,7 +100,7 @@ class DefaultTranscodeEngine(
      * We don't have to worry about which tracks are available and how. The [Segments] class
      * will simply return null if there's nothing to be done.
      */
-    override fun transcode(progress: (Double) -> Unit) {
+    override fun transcode(context: Context, progress: (Double) -> Unit) {
         var loop = 0L
         log.i("transcode(): about to start, " +
                 "durationUs=${timer.totalDurationUs}, " +
@@ -109,8 +111,8 @@ class DefaultTranscodeEngine(
             // Create both segments before reading. Creating the segment calls source.selectTrack,
             // and if source is the same, it's important that both tracks are selected before
             // reading (or even worse, seeking. DataSource.seek is broken if you add a track later on).
-            val audio = segments.next(TrackType.AUDIO)
-            val video = segments.next(TrackType.VIDEO)
+            val audio = segments.next(context,TrackType.AUDIO)
+            val video = segments.next(context,TrackType.VIDEO)
             val advanced = (audio?.advance() ?: false) or (video?.advance() ?: false)
             val completed = !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
index a04c5e35..c16f0a5c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
@@ -1,5 +1,6 @@
 package com.otaliastudios.transcoder.internal.transcode
 
+import android.content.Context
 import com.otaliastudios.transcoder.Transcoder
 import com.otaliastudios.transcoder.TranscoderOptions
 import com.otaliastudios.transcoder.internal.DataSources
@@ -10,7 +11,7 @@ abstract class TranscodeEngine {
 
     abstract fun validate(): Boolean
 
-    abstract fun transcode(progress: (Double) -> Unit)
+    abstract fun transcode(context: Context,progress: (Double) -> Unit)
 
     abstract fun cleanup()
 
@@ -24,7 +25,7 @@ abstract class TranscodeEngine {
         }
 
         @JvmStatic
-        fun transcode(options: TranscoderOptions) {
+        fun transcode(context: Context, options: TranscoderOptions) {
             log.i("transcode(): called...")
             var engine: TranscodeEngine? = null
             val dispatcher = TranscodeDispatcher(options)
@@ -45,7 +46,7 @@ abstract class TranscodeEngine {
                 if (!engine.validate()) {
                     dispatcher.dispatchSuccess(Transcoder.SUCCESS_NOT_NEEDED)
                 } else {
-                    engine.transcode {
+                    engine.transcode(context) {
                         dispatcher.dispatchProgress(it)
                     }
                     dispatcher.dispatchSuccess(Transcoder.SUCCESS_TRANSCODED)

From 6dc53f37caf9864f75de66622773af15c1a2a666 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Thu, 27 May 2021 18:54:45 +0530
Subject: [PATCH 004/101] move renderer and driver code outside

---
 lib/build.gradle.kts                          |   4 -
 .../transcoder/custom/OtaliaDriver.kt         |  42 ------
 .../transcoder/custom/OtaliaExporterNew.kt    |  40 ------
 .../otaliastudios/transcoder/custom/RKorge.kt | 124 ------------------
 .../transcoder/internal/pipeline/pipelines.kt |  47 ++++---
 5 files changed, 29 insertions(+), 228 deletions(-)
 delete mode 100644 lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaDriver.kt
 delete mode 100644 lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaExporterNew.kt
 delete mode 100644 lib/src/main/java/com/otaliastudios/transcoder/custom/RKorge.kt

diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index 52561f7d..a670c667 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -30,10 +30,6 @@ dependencies {
     androidTestImplementation("androidx.test:rules:1.3.0")
     androidTestImplementation("androidx.test.ext:junit:1.1.2")
     androidTestImplementation("org.mockito:mockito-android:2.28.2")
-    implementation(project(":shared:features:avcore"))
-    implementation(project(":shared:features:windowsystem"))
-    api(project(":shared:renderer"))
-    implementation(Korlibs.korge)
 }
 
 publisher {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaDriver.kt b/lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaDriver.kt
deleted file mode 100644
index 21a5c163..00000000
--- a/lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaDriver.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.otaliastudios.transcoder.custom
-
-import android.graphics.Point
-import com.otaliastudios.transcoder.internal.pipeline.BaseStep
-import com.otaliastudios.transcoder.internal.pipeline.Channel
-import com.otaliastudios.transcoder.internal.pipeline.State
-import com.otaliastudios.transcoder.internal.utils.Logger
-import io.invideo.features.avcore.VideoConfig
-import kotlin.time.ExperimentalTime
-
-@OptIn(ExperimentalTime::class)
-class OtaliaDriver(private val videoConfig: VideoConfig) :
-    BaseStep<Unit, Channel, RenderingData, RenderingChannel>() {
-
-    override val channel = Channel
-    private val log = Logger("Driver")
-
-    private var timestampMs: Long = 0
-    private var prevTimestampMs: Long = 0
-
-    private var frameCount: Int = 0
-
-    override fun initialize(next: RenderingChannel) {
-        super.initialize(next)
-        next.setDimensions(Point(videoConfig.width, videoConfig.height))
-    }
-
-    override fun step(state: State.Ok<Unit>, fresh: Boolean): State<RenderingData> {
-        return if (timestampMs > videoConfig.duration) {
-            log.i("Source is drained! Returning Eos as soon as possible.")
-            State.Eos(RenderingData(timestampMs, prevTimestampMs) {})
-        } else {
-            val data = RenderingData(timestampMs, prevTimestampMs) {
-                prevTimestampMs = timestampMs
-                frameCount++
-                timestampMs = frameCount * 1000L / videoConfig.fps
-            }
-            return State.Ok(data)
-        }
-
-    }
-}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaExporterNew.kt b/lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaExporterNew.kt
deleted file mode 100644
index 8c34d16c..00000000
--- a/lib/src/main/java/com/otaliastudios/transcoder/custom/OtaliaExporterNew.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.otaliastudios.transcoder.custom
-
-import android.content.Context
-import com.otaliastudios.transcoder.Transcoder
-import com.otaliastudios.transcoder.TranscoderListener
-import com.otaliastudios.transcoder.resize.PassThroughResizer
-import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
-import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy
-
-class OtaliaExporterNew {
-
-    fun start(context: Context) {
-        val builder = Transcoder.into("/sdcard/transcoder.mp4")
-        builder.setListener(object : TranscoderListener {
-            override fun onTranscodeProgress(progress: Double) {
-            }
-
-            override fun onTranscodeCompleted(successCode: Int) {
-            }
-
-            override fun onTranscodeCanceled() {
-            }
-
-            override fun onTranscodeFailed(exception: Throwable) {
-            }
-        })
-            .addDataSource("/sdcard/big_bunny.mp4")// necessary to add but not used
-            .setAudioTrackStrategy(RemoveTrackStrategy())
-            .setVideoTrackStrategy(
-                DefaultVideoStrategy.Builder()
-                    .addResizer(PassThroughResizer())
-                    .frameRate(30)
-                    .build()
-            )
-            .build()
-        builder.transcode(context)
-
-
-    }
-}
\ No newline at end of file
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/custom/RKorge.kt b/lib/src/main/java/com/otaliastudios/transcoder/custom/RKorge.kt
deleted file mode 100644
index 2586006e..00000000
--- a/lib/src/main/java/com/otaliastudios/transcoder/custom/RKorge.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-package com.otaliastudios.transcoder.custom
-
-import android.content.Context
-import android.graphics.Point
-import com.otaliastudios.transcoder.internal.pipeline.Channel
-import com.otaliastudios.transcoder.internal.pipeline.State
-import com.otaliastudios.transcoder.internal.pipeline.Step
-import io.invideo.renderer.IVRootGameMainAndroid
-import io.invideo.renderer.RenderTreeEvent
-import io.invideo.renderer.RendererConfiguration
-import io.invideo.renderer.SceneTimeUpdateEvent
-import io.invideo.renderer.UpdateResourceEvent
-import io.invideo.renderer.VideoPlayerFactoryInterface
-import io.invideo.renderer.VideoSeekEvent
-import io.invideo.renderer.extensions.dispatchRenderTreeEvent
-import io.invideo.renderer.extensions.dispatchSceneTimeUpdateEvent
-import io.invideo.renderer.extensions.dispatchUpdateResourcesEvent
-import io.invideo.renderer.extensions.dispatchVideoSeekEvent
-import io.invideo.renderer.getTimeline
-import io.invideo.renderer.rendertree.Node
-import io.invideo.renderer.rendertree.RenderTree
-import io.invideo.renderer.timeline.IVEventDispatcher
-import io.invideo.renderer.timeline.TimeLinePlayer
-import io.invideo.renderer.timeline.VideoEventDispatcher
-import kotlin.time.ExperimentalTime
-
-data class RenderingData(
-    val timeMsCurrent: Long,
-    val timeMsPrevious: Long,
-    val updateTime: () -> Unit
-)
-
-interface RenderingChannel : Channel {
-    fun setDimensions(point: Point)
-}
-
-@OptIn(ExperimentalTime::class)
-class RKorge(
-    context: Context,
-    config: RendererConfiguration,
-    videoPlayerFactoryInterface: VideoPlayerFactoryInterface?
-) : Step<RenderingData, RenderingChannel, Long, Channel>, RenderingChannel {
-    private var isInitialized = false
-    var reshape = true
-
-    val rootGameMain = IVRootGameMainAndroid(context, config, videoPlayerFactoryInterface)
-    private var oldBoundSize = Point(0, 0)
-
-    private val timelinePlayer: TimeLinePlayer
-    private val sceneTimeUpdateEvent = SceneTimeUpdateEvent(timeInSeconds = 0.0)
-    lateinit var size: Point
-    override val channel = this
-
-    init {
-        timelinePlayer = TimeLinePlayer(getTimeline(), EventDispatcher())
-    }
-
-    override fun step(state: State.Ok<RenderingData>, fresh: Boolean): State<Long> {
-        val (timeMsCurrent, timeMsPrevious) = state.value
-
-        return if (state is State.Eos) {
-            State.Eos(0L)
-        } else {
-            sceneTimeUpdateEvent.timeInSeconds = timeMsCurrent / 1000.0
-            rootGameMain.gameWindow.dispatchSceneTimeUpdateEvent(sceneTimeUpdateEvent)
-
-            timelinePlayer.checkAndUpdate(timeMsPrevious / 1000.0, timeMsCurrent / 1000.0, false)
-            render(size, 1.0f)
-            state.value.updateTime()
-            State.Ok(state.value.timeMsCurrent * 1000L)
-        }
-
-    }
-
-    override fun setDimensions(point: Point) {
-        size = point
-    }
-
-    fun render(boundSize: Point, contentScaleFactor: Float = 1.0f) {
-        if (!isInitialized) {
-            isInitialized = true
-            rootGameMain.preRunMain()
-            rootGameMain.gameWindow.dispatchInitEvent()
-            rootGameMain.runMain()
-            reshape = true
-        }
-
-        if (!reshape && oldBoundSize.equals(boundSize.x, boundSize.y)) {
-            reshape = true
-        }
-        oldBoundSize = boundSize
-
-        if (reshape) {
-            reshape = false
-            val width = (boundSize.x * contentScaleFactor).toInt()
-            val height = (boundSize.y * contentScaleFactor).toInt()
-            rootGameMain.gameWindow.dispatchReshapeEvent(0, 0, width, height)
-        }
-
-        rootGameMain.gameWindow.frame()
-    }
-
-    inner class EventDispatcher : IVEventDispatcher {
-        override fun updateResources(node: Node, shouldLoad: Boolean) {
-            val event = UpdateResourceEvent(node, shouldLoad)
-            rootGameMain.gameWindow.dispatchUpdateResourcesEvent(event)
-        }
-
-        override fun updateRenderTree(renderTree: RenderTree) {
-            val event = RenderTreeEvent(renderTree)
-            rootGameMain.gameWindow.dispatchRenderTreeEvent(event)
-        }
-
-        override fun play() {
-        }
-
-        override fun pause() {
-        }
-
-        override fun seek(seekTimeMs: Long) {
-            rootGameMain.gameWindow.dispatchVideoSeekEvent(VideoSeekEvent(seekTimeMs))
-        }
-    }
-}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
index e89f0688..f870f536 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
@@ -1,18 +1,20 @@
 package com.otaliastudios.transcoder.internal.pipeline
 
 import android.content.Context
+import android.graphics.Point
 import android.media.MediaFormat
-import android.util.Log
 import com.otaliastudios.transcoder.common.TrackType
 import com.otaliastudios.transcoder.internal.Codecs
 import com.otaliastudios.transcoder.internal.audio.AudioEngine
-import com.otaliastudios.transcoder.internal.data.*
-import com.otaliastudios.transcoder.internal.data.Reader
-import com.otaliastudios.transcoder.internal.data.ReaderTimer
-import com.otaliastudios.transcoder.internal.data.Writer
 import com.otaliastudios.transcoder.internal.codec.Decoder
+import com.otaliastudios.transcoder.internal.codec.DecoderChannel
+import com.otaliastudios.transcoder.internal.codec.DecoderData
 import com.otaliastudios.transcoder.internal.codec.DecoderTimer
 import com.otaliastudios.transcoder.internal.codec.Encoder
+import com.otaliastudios.transcoder.internal.data.Bridge
+import com.otaliastudios.transcoder.internal.data.Reader
+import com.otaliastudios.transcoder.internal.data.ReaderTimer
+import com.otaliastudios.transcoder.internal.data.Writer
 import com.otaliastudios.transcoder.internal.video.VideoPublisher
 import com.otaliastudios.transcoder.internal.video.VideoRenderer
 import com.otaliastudios.transcoder.resample.AudioResampler
@@ -20,12 +22,6 @@ import com.otaliastudios.transcoder.sink.DataSink
 import com.otaliastudios.transcoder.source.DataSource
 import com.otaliastudios.transcoder.stretch.AudioStretcher
 import com.otaliastudios.transcoder.time.TimeInterpolator
-import io.invideo.features.avcore.VideoConfig
-import com.otaliastudios.transcoder.custom.OtaliaDriver
-import com.otaliastudios.transcoder.custom.RKorge
-import io.invideo.renderer.RenderTarget
-import io.invideo.renderer.RendererConfiguration
-import io.invideo.renderer.rendertree.Size
 
 internal fun EmptyPipeline() = Pipeline.build("Empty")
 
@@ -57,6 +53,26 @@ internal fun RegularPipeline(
     TrackType.AUDIO -> AudioPipeline(source, sink, interpolator, format, codecs, audioStretcher, audioResampler)
 }
 
+data class RenderingData(
+        val timeMsCurrent: Long,
+        val timeMsPrevious: Long,
+        val updateTime: () -> Unit
+)
+
+interface RenderingChannel : Channel {
+        fun setDimensions(point: Point)
+}
+lateinit var korgeDriver: Step<Unit, Channel, RenderingData, RenderingChannel>
+lateinit var korgeRenderer: Step<RenderingData, RenderingChannel, Long, Channel>
+
+fun setDriverStep(driver: Step<Unit, Channel, RenderingData, RenderingChannel>) {
+        korgeDriver = driver
+}
+
+fun setRendererStep(korgeR: Step<RenderingData, RenderingChannel, Long, Channel>){
+        korgeRenderer = korgeR
+}
+
 private fun KorgeVideoPipeline(
         context: Context,
         source: DataSource,
@@ -67,13 +83,8 @@ private fun KorgeVideoPipeline(
         videoRotation: Int
 ) = Pipeline.build("Video") {
 
-        // Driver -> Renderer ->
-        OtaliaDriver(VideoConfig(720, 720, 10 * 1024 * 1024, duration = 20000, 30)) +
-                RKorge(context, RendererConfiguration(
-                        Size(720.0, 720.0),
-                        RenderTarget.DISPLAY
-                ) { Log.d("Pipeline", "Renderer Initialization complete") }, null
-                ) +
+        korgeDriver +
+                korgeRenderer +
                 VideoPublisher() +
                 Encoder(codecs, TrackType.VIDEO) +
                 Writer(sink, TrackType.VIDEO)

From a005ac09ead762ece32257b4493c9a27d2513b9d Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Sat, 29 May 2021 14:56:56 +0530
Subject: [PATCH 005/101] letting video format be fetched from source

---
 .../transcoder/internal/Codecs.kt             | 20 +++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
index e9bb7f55..6fdaebac 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
@@ -42,16 +42,16 @@ class Codecs(
         }
 
         private val lazyVideo by lazy {
-//            val format = tracks.outputFormats.video
-            val format = MediaFormat.createVideoFormat("video/avc", 720, 720)
-            format.setInteger(
-                MediaFormat.KEY_COLOR_FORMAT,
-                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
-            )
-            format.setInteger(MediaFormat.KEY_BIT_RATE, 10 * 1024 * 1024 /*calcBitRate()*/)
-            format.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
-            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
-            format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 720 * 720)
+            val format = tracks.outputFormats.video
+//            val format = MediaFormat.createVideoFormat("video/avc", 720, 720)
+//            format.setInteger(
+//                MediaFormat.KEY_COLOR_FORMAT,
+//                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
+//            )
+//            format.setInteger(MediaFormat.KEY_BIT_RATE, 10 * 1024 * 1024 /*calcBitRate()*/)
+//            format.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
+//            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
+//            format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 720 * 720)
 
 
             val codec = MediaCodec.createEncoderByType(format.getString(MediaFormat.KEY_MIME)!!)

From 576c308db21f6d544ce3e066aaec332cdb243e4c Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Sat, 29 May 2021 15:03:45 +0530
Subject: [PATCH 006/101] remove context reference inside library

---
 .../java/com/otaliastudios/transcoder/Thumbnailer.kt |  7 +++----
 .../otaliastudios/transcoder/ThumbnailerOptions.kt   |  4 ++--
 .../com/otaliastudios/transcoder/Transcoder.java     |  4 ++--
 .../otaliastudios/transcoder/TranscoderOptions.java  |  4 ++--
 .../otaliastudios/transcoder/internal/Segments.kt    | 12 +++++-------
 .../transcoder/internal/pipeline/pipelines.kt        |  7 +------
 .../internal/thumbnails/DefaultThumbnailsEngine.kt   |  6 ++----
 .../internal/thumbnails/ThumbnailsEngine.kt          |  7 +++----
 .../internal/transcode/DefaultTranscodeEngine.kt     | 10 ++++------
 .../transcoder/internal/transcode/TranscodeEngine.kt |  7 +++----
 10 files changed, 27 insertions(+), 41 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt b/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt
index 1f70b655..c519e49a 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt
@@ -1,6 +1,5 @@
 package com.otaliastudios.transcoder
 
-import android.content.Context
 import com.otaliastudios.transcoder.internal.thumbnails.ThumbnailsEngine
 import com.otaliastudios.transcoder.internal.utils.ThreadPool
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
@@ -9,14 +8,14 @@ import java.util.concurrent.Future
 
 class Thumbnailer private constructor() {
 
-    fun thumbnails(context: Context,options: ThumbnailerOptions): Future<Void> {
+    fun thumbnails(options: ThumbnailerOptions): Future<Void> {
         return ThreadPool.executor.submit(Callable {
-            ThumbnailsEngine.thumbnails(context,options)
+            ThumbnailsEngine.thumbnails(options)
             null
         })
     }
 
-    fun thumbnails(context: Context,builder: ThumbnailerOptions.Builder.() -> Unit) = thumbnails(context,
+    fun thumbnails(builder: ThumbnailerOptions.Builder.() -> Unit) = thumbnails(
             options = ThumbnailerOptions.Builder().apply(builder).build()
     )
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
index 14953331..5a0a35d6 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
@@ -104,8 +104,8 @@ class ThumbnailerOptions(
             )
         }
 
-        fun thumbnails(context: Context): Future<Void> {
-            return Thumbnailer.getInstance().thumbnails(context,build())
+        fun thumbnails(): Future<Void> {
+            return Thumbnailer.getInstance().thumbnails(build())
         }
     }
 }
\ No newline at end of file
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java b/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java
index afbc305a..3b41eefd 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java
@@ -95,9 +95,9 @@ public static TranscoderOptions.Builder into(@NonNull DataSink dataSink) {
      * @return a Future that completes when transcoding is completed
      */
     @NonNull
-    public Future<Void> transcode(Context context, @NonNull final TranscoderOptions options) {
+    public Future<Void> transcode(@NonNull final TranscoderOptions options) {
         return ThreadPool.getExecutor().submit(() -> {
-            TranscodeEngine.transcode(context, options);
+            TranscodeEngine.transcode(options);
             return null;
         });
     }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
index 24369255..42b35679 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
@@ -386,8 +386,8 @@ public TranscoderOptions build() {
         }
 
         @NonNull
-        public Future<Void> transcode(Context context) {
-            return Transcoder.getInstance().transcode(context, build());
+        public Future<Void> transcode() {
+            return Transcoder.getInstance().transcode(build());
         }
     }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
index 272bb161..10e0e350 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
@@ -1,6 +1,5 @@
 package com.otaliastudios.transcoder.internal
 
-import android.content.Context
 import android.media.MediaFormat
 import com.otaliastudios.transcoder.common.TrackStatus
 import com.otaliastudios.transcoder.common.TrackType
@@ -11,7 +10,7 @@ import com.otaliastudios.transcoder.internal.utils.mutableTrackMapOf
 class Segments(
         private val sources: DataSources,
         private val tracks: Tracks,
-        private val factory: (Context, TrackType, Int, TrackStatus, MediaFormat) -> Pipeline
+        private val factory: (TrackType, Int, TrackStatus, MediaFormat) -> Pipeline
 ) {
 
     private val log = Logger("Segments")
@@ -34,7 +33,7 @@ class Segments(
      * - data sources don't have this type
      * - transcoding for this track is over
      */
-    fun next(context: Context,type: TrackType): Segment? {
+    fun next(type: TrackType): Segment? {
         val currentIndex = currentIndex[type]
         val requestedIndex = requestedIndex[type]
         return when {
@@ -43,7 +42,7 @@ class Segments(
             }
             // We need to open a new segment, if possible.
             // createSegment will make requested == current.
-            requestedIndex > currentIndex -> tryCreateSegment(context, type, requestedIndex)
+            requestedIndex > currentIndex -> tryCreateSegment(type, requestedIndex)
             // requested == current
             current[type].canAdvance() -> current[type]
             // requested == current, but this segment is finished:
@@ -51,7 +50,7 @@ class Segments(
             // so if this is the last one, we'll return null.
             else -> {
                 destroySegment(current[type])
-                next(context,type)
+                next(type)
             }
         }
     }
@@ -61,7 +60,7 @@ class Segments(
         current.audioOrNull()?.let { destroySegment(it) }
     }
 
-    private fun tryCreateSegment(context: Context,type: TrackType, index: Int): Segment? {
+    private fun tryCreateSegment(type: TrackType, index: Int): Segment? {
         // Return null if out of bounds, either because segments are over or because the
         // source set does not have sources for this track type.
         val source = sources[type].getOrNull(index) ?: return null
@@ -85,7 +84,6 @@ class Segments(
         // who check it during pipeline init.
         currentIndex[type] = index
         val pipeline = factory(
-            context,
                 type,
                 index,
                 tracks.all[type],
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
index f870f536..10a40ea3 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
@@ -1,14 +1,11 @@
 package com.otaliastudios.transcoder.internal.pipeline
 
-import android.content.Context
 import android.graphics.Point
 import android.media.MediaFormat
 import com.otaliastudios.transcoder.common.TrackType
 import com.otaliastudios.transcoder.internal.Codecs
 import com.otaliastudios.transcoder.internal.audio.AudioEngine
 import com.otaliastudios.transcoder.internal.codec.Decoder
-import com.otaliastudios.transcoder.internal.codec.DecoderChannel
-import com.otaliastudios.transcoder.internal.codec.DecoderData
 import com.otaliastudios.transcoder.internal.codec.DecoderTimer
 import com.otaliastudios.transcoder.internal.codec.Encoder
 import com.otaliastudios.transcoder.internal.data.Bridge
@@ -38,7 +35,6 @@ internal fun PassThroughPipeline(
 }
 
 internal fun RegularPipeline(
-        context: Context,
         track: TrackType,
         source: DataSource,
         sink: DataSink,
@@ -49,7 +45,7 @@ internal fun RegularPipeline(
         audioStretcher: AudioStretcher,
         audioResampler: AudioResampler
 ) = when (track) {
-    TrackType.VIDEO -> KorgeVideoPipeline(context,source, sink, interpolator, format, codecs, videoRotation)
+    TrackType.VIDEO -> KorgeVideoPipeline(source, sink, interpolator, format, codecs, videoRotation)
     TrackType.AUDIO -> AudioPipeline(source, sink, interpolator, format, codecs, audioStretcher, audioResampler)
 }
 
@@ -74,7 +70,6 @@ fun setRendererStep(korgeR: Step<RenderingData, RenderingChannel, Long, Channel>
 }
 
 private fun KorgeVideoPipeline(
-        context: Context,
         source: DataSource,
         sink: DataSink,
         interpolator: TimeInterpolator,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 664467c2..df038fc5 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -1,6 +1,5 @@
 package com.otaliastudios.transcoder.internal.thumbnails
 
-import android.content.Context
 import android.graphics.Bitmap
 import android.media.MediaFormat
 import android.media.MediaFormat.KEY_HEIGHT
@@ -77,7 +76,6 @@ class DefaultThumbnailsEngine(
     }
 
     private fun createPipeline(
-        context: Context,
         type: TrackType,
         index: Int,
         status: TrackStatus,
@@ -115,10 +113,10 @@ class DefaultThumbnailsEngine(
 
     private lateinit var progress: (Thumbnail) -> Unit
 
-    override fun thumbnails(context: Context,progress: (Thumbnail) -> Unit) {
+    override fun thumbnails(progress: (Thumbnail) -> Unit) {
         this.progress = progress
         while (true) {
-            val advanced = segments.next(context,TrackType.VIDEO)?.advance() ?: false
+            val advanced = segments.next(TrackType.VIDEO)?.advance() ?: false
             val completed = !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
             if (Thread.interrupted()) {
                 throw InterruptedException()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index 7fc312d7..7ba13e4d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -1,6 +1,5 @@
 package com.otaliastudios.transcoder.internal.thumbnails
 
-import android.content.Context
 import com.otaliastudios.transcoder.ThumbnailerOptions
 import com.otaliastudios.transcoder.Transcoder
 import com.otaliastudios.transcoder.TranscoderOptions
@@ -11,7 +10,7 @@ import com.otaliastudios.transcoder.thumbnail.Thumbnail
 
 abstract class ThumbnailsEngine {
 
-    abstract fun thumbnails(context: Context,progress: (Thumbnail) -> Unit)
+    abstract fun thumbnails(progress: (Thumbnail) -> Unit)
 
     abstract fun cleanup()
 
@@ -25,7 +24,7 @@ abstract class ThumbnailsEngine {
         }
 
         @JvmStatic
-        fun thumbnails(context: Context,options: ThumbnailerOptions) {
+        fun thumbnails(options: ThumbnailerOptions) {
             log.i("thumbnails(): called...")
             var engine: ThumbnailsEngine? = null
             val dispatcher = ThumbnailsDispatcher(options)
@@ -36,7 +35,7 @@ abstract class ThumbnailsEngine {
                         resizer = options.resizer,
                         requests = options.thumbnailRequests
                 )
-                engine.thumbnails(context) {
+                engine.thumbnails {
                     dispatcher.dispatchThumbnail(it)
                 }
                 dispatcher.dispatchCompletion()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
index 7268493f..a9c334a3 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
@@ -1,6 +1,5 @@
 package com.otaliastudios.transcoder.internal.transcode
 
-import android.content.Context
 import android.media.MediaFormat
 import com.otaliastudios.transcoder.common.TrackStatus
 import com.otaliastudios.transcoder.common.TrackType
@@ -62,7 +61,6 @@ class DefaultTranscodeEngine(
     }
 
     private fun createPipeline(
-        context: Context,
         type: TrackType,
         index: Int,
         status: TrackStatus,
@@ -81,7 +79,7 @@ class DefaultTranscodeEngine(
             TrackStatus.ABSENT -> EmptyPipeline()
             TrackStatus.REMOVING -> EmptyPipeline()
             TrackStatus.PASS_THROUGH -> PassThroughPipeline(type, source, sink, interpolator)
-            TrackStatus.COMPRESSING -> RegularPipeline(context,type,
+            TrackStatus.COMPRESSING -> RegularPipeline(type,
                     source, sink, interpolator, outputFormat, codecs,
                     videoRotation, audioStretcher, audioResampler)
         }
@@ -100,7 +98,7 @@ class DefaultTranscodeEngine(
      * We don't have to worry about which tracks are available and how. The [Segments] class
      * will simply return null if there's nothing to be done.
      */
-    override fun transcode(context: Context, progress: (Double) -> Unit) {
+    override fun transcode(progress: (Double) -> Unit) {
         var loop = 0L
         log.i("transcode(): about to start, " +
                 "durationUs=${timer.totalDurationUs}, " +
@@ -111,8 +109,8 @@ class DefaultTranscodeEngine(
             // Create both segments before reading. Creating the segment calls source.selectTrack,
             // and if source is the same, it's important that both tracks are selected before
             // reading (or even worse, seeking. DataSource.seek is broken if you add a track later on).
-            val audio = segments.next(context,TrackType.AUDIO)
-            val video = segments.next(context,TrackType.VIDEO)
+            val audio = segments.next(TrackType.AUDIO)
+            val video = segments.next(TrackType.VIDEO)
             val advanced = (audio?.advance() ?: false) or (video?.advance() ?: false)
             val completed = !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
index c16f0a5c..a04c5e35 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
@@ -1,6 +1,5 @@
 package com.otaliastudios.transcoder.internal.transcode
 
-import android.content.Context
 import com.otaliastudios.transcoder.Transcoder
 import com.otaliastudios.transcoder.TranscoderOptions
 import com.otaliastudios.transcoder.internal.DataSources
@@ -11,7 +10,7 @@ abstract class TranscodeEngine {
 
     abstract fun validate(): Boolean
 
-    abstract fun transcode(context: Context,progress: (Double) -> Unit)
+    abstract fun transcode(progress: (Double) -> Unit)
 
     abstract fun cleanup()
 
@@ -25,7 +24,7 @@ abstract class TranscodeEngine {
         }
 
         @JvmStatic
-        fun transcode(context: Context, options: TranscoderOptions) {
+        fun transcode(options: TranscoderOptions) {
             log.i("transcode(): called...")
             var engine: TranscodeEngine? = null
             val dispatcher = TranscodeDispatcher(options)
@@ -46,7 +45,7 @@ abstract class TranscodeEngine {
                 if (!engine.validate()) {
                     dispatcher.dispatchSuccess(Transcoder.SUCCESS_NOT_NEEDED)
                 } else {
-                    engine.transcode(context) {
+                    engine.transcode {
                         dispatcher.dispatchProgress(it)
                     }
                     dispatcher.dispatchSuccess(Transcoder.SUCCESS_TRANSCODED)

From fc33e16ee23cd89f22e97be76665e2e83d9f37b5 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Sun, 30 May 2021 17:25:44 +0530
Subject: [PATCH 007/101] let codec info be fetched from source builder setup

---
 .../com/otaliastudios/transcoder/internal/Codecs.kt    | 10 ----------
 .../transcoder/internal/video/FrameDropper.kt          |  2 +-
 2 files changed, 1 insertion(+), 11 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
index 6fdaebac..bfc471b6 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
@@ -43,16 +43,6 @@ class Codecs(
 
         private val lazyVideo by lazy {
             val format = tracks.outputFormats.video
-//            val format = MediaFormat.createVideoFormat("video/avc", 720, 720)
-//            format.setInteger(
-//                MediaFormat.KEY_COLOR_FORMAT,
-//                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
-//            )
-//            format.setInteger(MediaFormat.KEY_BIT_RATE, 10 * 1024 * 1024 /*calcBitRate()*/)
-//            format.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
-//            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
-//            format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 720 * 720)
-
 
             val codec = MediaCodec.createEncoderByType(format.getString(MediaFormat.KEY_MIME)!!)
             codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt
index fea36f62..1b95b7c8 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt
@@ -10,7 +10,7 @@ interface FrameDropper {
  * A very simple dropper, from
  * https://stackoverflow.com/questions/4223766/dropping-video-frames
  */
-internal fun FrameDropper(inputFps: Int, outputFps: Int) = object : FrameDropper {
+fun FrameDropper(inputFps: Int, outputFps: Int) = object : FrameDropper {
 
     private val log = Logger("FrameDropper")
     private val inputSpf = 1.0 / inputFps

From 3b8bd8d25be444dd9f6739ee8451e2e09a09cf6e Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Sun, 30 May 2021 17:49:31 +0530
Subject: [PATCH 008/101] make datasources constructor public

---
 .../java/com/otaliastudios/transcoder/internal/DataSources.kt   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
index 225d746b..85bab866 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
@@ -9,7 +9,7 @@ import com.otaliastudios.transcoder.internal.utils.trackMapOf
 import com.otaliastudios.transcoder.source.BlankAudioDataSource
 import com.otaliastudios.transcoder.source.DataSource
 
-class DataSources private constructor(
+class DataSources constructor(
         videoSources: List<DataSource>,
         audioSources: List<DataSource>,
 ) : TrackMap<List<DataSource>> {

From c3938d6830c2a41709f6d3c636734b205e3a31b7 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Mon, 31 May 2021 19:52:58 +0530
Subject: [PATCH 009/101] make frame dropper timestamp aware

---
 .../otaliastudios/transcoder/internal/video/FrameDropper.kt  | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt
index 1b95b7c8..f8ca5f86 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt
@@ -17,9 +17,12 @@ fun FrameDropper(inputFps: Int, outputFps: Int) = object : FrameDropper {
     private val outputSpf = 1.0 / outputFps
     private var currentSpf = 0.0
     private var frameCount = 0
+    private var previousTs = 0.0
 
     override fun shouldRender(timeUs: Long): Boolean {
-        currentSpf += inputSpf
+        val timeS = timeUs / 1000.0 / 1000.0
+        currentSpf += timeS - previousTs
+        previousTs = timeS
         if (frameCount++ == 0) {
             log.v("RENDERING (first frame) - currentSpf=$currentSpf inputSpf=$inputSpf outputSpf=$outputSpf")
             return true

From 6594713949add9376f82d65d02f05519b0f47bb2 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Tue, 1 Jun 2021 23:28:31 +0530
Subject: [PATCH 010/101] implement read track in trim source

---
 .../com/otaliastudios/transcoder/source/TrimDataSource.java | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/TrimDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/TrimDataSource.java
index 28f1b788..8c17c428 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/TrimDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/TrimDataSource.java
@@ -97,6 +97,12 @@ public boolean canReadTrack(@NonNull TrackType type) {
         return super.canReadTrack(type);
     }
 
+    @Override
+    public void readTrack(@NonNull Chunk chunk) {
+        super.readTrack(chunk);
+        chunk.timeUs = chunk.timeUs - trimStartUs;
+    }
+
     @Override
     public boolean isDrained() {
         // Enforce the trim end: this works thanks to the fact that extraDurationUs is added

From 9e50946bded023bb0fafd1d529ad9199c301431a Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Fri, 4 Jun 2021 14:30:52 +0530
Subject: [PATCH 011/101] pass pipeline from exporter

---
 .../otaliastudios/transcoder/Transcoder.java  | 18 ++++++---
 .../transcoder/TranscoderOptions.java         | 15 +++++---
 .../transcoder/internal/pipeline/pipelines.kt | 37 ++++++-------------
 .../transcode/DefaultTranscodeEngine.kt       | 22 ++++++-----
 .../internal/transcode/TranscodeEngine.kt     | 12 +++++-
 .../sink/InvalidOutputFormatException.java    |  2 +-
 6 files changed, 57 insertions(+), 49 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java b/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java
index 3b41eefd..a1b901a7 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java
@@ -15,9 +15,16 @@
  */
 package com.otaliastudios.transcoder;
 
-import android.content.Context;
+import android.media.MediaFormat;
 import android.os.Build;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.otaliastudios.transcoder.common.TrackStatus;
+import com.otaliastudios.transcoder.common.TrackType;
+import com.otaliastudios.transcoder.internal.Codecs;
+import com.otaliastudios.transcoder.internal.pipeline.Pipeline;
 import com.otaliastudios.transcoder.internal.transcode.TranscodeEngine;
 import com.otaliastudios.transcoder.internal.utils.ThreadPool;
 import com.otaliastudios.transcoder.sink.DataSink;
@@ -26,8 +33,8 @@
 import java.io.FileDescriptor;
 import java.util.concurrent.Future;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
+import kotlin.jvm.functions.Function3;
+import kotlin.jvm.functions.Function4;
 
 public class Transcoder {
     /**
@@ -92,12 +99,13 @@ public static TranscoderOptions.Builder into(@NonNull DataSink dataSink) {
      * Transcodes video file asynchronously.
      *
      * @param options The transcoder options.
+     * @param function
      * @return a Future that completes when transcoding is completed
      */
     @NonNull
-    public Future<Void> transcode(@NonNull final TranscoderOptions options) {
+    public Future<Void> transcode(@NonNull final TranscoderOptions options, Function3<? super TrackType, ? super DataSink, ? super Codecs, Pipeline> function) {
         return ThreadPool.getExecutor().submit(() -> {
-            TranscodeEngine.transcode(options);
+            TranscodeEngine.transcode(options, function);
             return null;
         });
     }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
index 42b35679..ba81677c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
@@ -6,7 +6,13 @@
 import android.os.Handler;
 import android.os.Looper;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
 import com.otaliastudios.transcoder.common.TrackType;
+import com.otaliastudios.transcoder.internal.Codecs;
+import com.otaliastudios.transcoder.internal.pipeline.Pipeline;
 import com.otaliastudios.transcoder.resample.AudioResampler;
 import com.otaliastudios.transcoder.resample.DefaultAudioResampler;
 import com.otaliastudios.transcoder.sink.DataSink;
@@ -14,7 +20,6 @@
 import com.otaliastudios.transcoder.source.DataSource;
 import com.otaliastudios.transcoder.source.FileDescriptorDataSource;
 import com.otaliastudios.transcoder.source.FilePathDataSource;
-import com.otaliastudios.transcoder.source.BlankAudioDataSource;
 import com.otaliastudios.transcoder.source.UriDataSource;
 import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy;
 import com.otaliastudios.transcoder.strategy.DefaultVideoStrategies;
@@ -32,9 +37,7 @@
 import java.util.List;
 import java.util.concurrent.Future;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
+import kotlin.jvm.functions.Function3;
 
 /**
  * Collects transcoding options consumed by {@link Transcoder}.
@@ -386,8 +389,8 @@ public TranscoderOptions build() {
         }
 
         @NonNull
-        public Future<Void> transcode() {
-            return Transcoder.getInstance().transcode(build());
+        public Future<Void> transcode(Function3<? super TrackType, ? super DataSink, ? super Codecs, Pipeline> function) {
+            return Transcoder.getInstance().transcode(build(), function);
         }
     }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
index 10a40ea3..dab75de9 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
@@ -20,7 +20,7 @@ import com.otaliastudios.transcoder.source.DataSource
 import com.otaliastudios.transcoder.stretch.AudioStretcher
 import com.otaliastudios.transcoder.time.TimeInterpolator
 
-internal fun EmptyPipeline() = Pipeline.build("Empty")
+ fun EmptyPipeline() = Pipeline.build("Empty")
 
 internal fun PassThroughPipeline(
         track: TrackType,
@@ -45,7 +45,7 @@ internal fun RegularPipeline(
         audioStretcher: AudioStretcher,
         audioResampler: AudioResampler
 ) = when (track) {
-    TrackType.VIDEO -> KorgeVideoPipeline(source, sink, interpolator, format, codecs, videoRotation)
+    TrackType.VIDEO -> VideoPipeline(source, sink, interpolator, format, codecs, videoRotation)
     TrackType.AUDIO -> AudioPipeline(source, sink, interpolator, format, codecs, audioStretcher, audioResampler)
 }
 
@@ -58,32 +58,17 @@ data class RenderingData(
 interface RenderingChannel : Channel {
         fun setDimensions(point: Point)
 }
-lateinit var korgeDriver: Step<Unit, Channel, RenderingData, RenderingChannel>
-lateinit var korgeRenderer: Step<RenderingData, RenderingChannel, Long, Channel>
+//lateinit var korgeDriver: Step<Unit, Channel, RenderingData, RenderingChannel>
+//lateinit var korgeRenderer: Step<RenderingData, RenderingChannel, Long, Channel>
 
-fun setDriverStep(driver: Step<Unit, Channel, RenderingData, RenderingChannel>) {
-        korgeDriver = driver
-}
-
-fun setRendererStep(korgeR: Step<RenderingData, RenderingChannel, Long, Channel>){
-        korgeRenderer = korgeR
-}
-
-private fun KorgeVideoPipeline(
-        source: DataSource,
-        sink: DataSink,
-        interpolator: TimeInterpolator,
-        format: MediaFormat,
-        codecs: Codecs,
-        videoRotation: Int
-) = Pipeline.build("Video") {
+//fun setDriverStep(driver: Step<Unit, Channel, RenderingData, RenderingChannel>) {
+//        korgeDriver = driver
+//}
+//
+//fun setRendererStep(korgeR: Step<RenderingData, RenderingChannel, Long, Channel>){
+//        korgeRenderer = korgeR
+//}
 
-        korgeDriver +
-                korgeRenderer +
-                VideoPublisher() +
-                Encoder(codecs, TrackType.VIDEO) +
-                Writer(sink, TrackType.VIDEO)
-}
 
 private fun VideoPipeline(
         source: DataSource,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
index a9c334a3..0183bb8b 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
@@ -25,15 +25,16 @@ import com.otaliastudios.transcoder.time.TimeInterpolator
 import com.otaliastudios.transcoder.validator.Validator
 
 class DefaultTranscodeEngine(
-        private val dataSources: DataSources,
-        private val dataSink: DataSink,
-        strategies: TrackMap<TrackStrategy>,
-        private val validator: Validator,
-        private val videoRotation: Int,
-        private val audioStretcher: AudioStretcher,
-        private val audioResampler: AudioResampler,
-        interpolator: TimeInterpolator
-) : TranscodeEngine() {
+    private val dataSources: DataSources,
+    private val dataSink: DataSink,
+    strategies: TrackMap<TrackStrategy>,
+    private val validator: Validator,
+    private val videoRotation: Int,
+    private val audioStretcher: AudioStretcher,
+    private val audioResampler: AudioResampler,
+    interpolator: TimeInterpolator,
+    private val pipelineFactory: ((TrackType, DataSink, Codecs) -> Pipeline)?,
+    ) : TranscodeEngine() {
 
     private val log = Logger("TranscodeEngine")
 
@@ -75,6 +76,9 @@ class DefaultTranscodeEngine(
             timer.positionUs[type] > timer.totalDurationUs + 100L
         }
         val sink = dataSink.ignoringEos { index < sources.lastIndex }
+        if (pipelineFactory != null)
+            return pipelineFactory.invoke(type, sink, codecs)
+
         return when (status) {
             TrackStatus.ABSENT -> EmptyPipeline()
             TrackStatus.REMOVING -> EmptyPipeline()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
index a04c5e35..da791a99 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
@@ -2,9 +2,13 @@ package com.otaliastudios.transcoder.internal.transcode
 
 import com.otaliastudios.transcoder.Transcoder
 import com.otaliastudios.transcoder.TranscoderOptions
+import com.otaliastudios.transcoder.common.TrackType
+import com.otaliastudios.transcoder.internal.Codecs
 import com.otaliastudios.transcoder.internal.DataSources
+import com.otaliastudios.transcoder.internal.pipeline.Pipeline
 import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.internal.utils.trackMapOf
+import com.otaliastudios.transcoder.sink.DataSink
 
 abstract class TranscodeEngine {
 
@@ -24,7 +28,10 @@ abstract class TranscodeEngine {
         }
 
         @JvmStatic
-        fun transcode(options: TranscoderOptions) {
+        fun transcode(
+            options: TranscoderOptions,
+            function: ((TrackType, DataSink, Codecs) -> Pipeline)?,
+            ) {
             log.i("transcode(): called...")
             var engine: TranscodeEngine? = null
             val dispatcher = TranscodeDispatcher(options)
@@ -40,7 +47,8 @@ abstract class TranscodeEngine {
                         videoRotation = options.videoRotation,
                         interpolator = options.timeInterpolator,
                         audioStretcher = options.audioStretcher,
-                        audioResampler = options.audioResampler
+                        audioResampler = options.audioResampler,
+                        pipelineFactory = function
                 )
                 if (!engine.validate()) {
                     dispatcher.dispatchSuccess(Transcoder.SUCCESS_NOT_NEEDED)
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/sink/InvalidOutputFormatException.java b/lib/src/main/java/com/otaliastudios/transcoder/sink/InvalidOutputFormatException.java
index 93f841d5..524ae18a 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/sink/InvalidOutputFormatException.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/sink/InvalidOutputFormatException.java
@@ -23,7 +23,7 @@
 
 /**
  * One of the exceptions possibly thrown by
- * {@link Transcoder#transcode(TranscoderOptions)}, which means it can be
+ * {@link Transcoder#transcode(TranscoderOptions, kotlin.jvm.functions.Function4)}, which means it can be
  * passed to {@link TranscoderListener#onTranscodeFailed(Throwable)}.
  */
 public class InvalidOutputFormatException extends RuntimeException {

From 5d817acc11d745cc7ae1bbb2de1cce153bcdb126 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Sat, 5 Jun 2021 14:12:29 +0530
Subject: [PATCH 012/101] remove buffer has remaining condition check in chunks
 as it breaks code

---
 .../com/otaliastudios/transcoder/internal/audio/chunks.kt    | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt
index 56f28ea1..9ecddc21 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt
@@ -1,5 +1,6 @@
 package com.otaliastudios.transcoder.internal.audio
 
+import android.util.Log
 import java.nio.ShortBuffer
 
 private data class Chunk(
@@ -25,7 +26,9 @@ class ChunkQueue(private val sampleRate: Int, private val channels: Int) {
     fun isEmpty() = queue.isEmpty()
 
     fun enqueue(buffer: ShortBuffer, timeUs: Long, timeStretch: Double, release: () -> Unit) {
-        require(buffer.hasRemaining())
+        Log.w("ChunkQueue", "enqueue: " + "buffer is empty")
+//        require(buffer.hasRemaining())
+
         queue.addLast(Chunk(buffer, timeUs, timeStretch, release))
     }
 

From 0a517049f462bdc6a5f8800ae6102ed57dd6fa49 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Mon, 7 Jun 2021 15:03:13 +0530
Subject: [PATCH 013/101] make step name not internal

---
 .../java/com/otaliastudios/transcoder/internal/pipeline/Step.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt
index 6b7385d6..d0af8b33 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt
@@ -20,4 +20,4 @@ interface Step<
     fun release() = Unit
 }
 
-internal val Step<*, *, *, *>.name get() = this::class.simpleName
\ No newline at end of file
+val Step<*, *, *, *>.name get() = this::class.simpleName
\ No newline at end of file

From 7fca1c712dc746aa9a3f89bcbb75e095e96c0dbe Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Tue, 8 Jun 2021 15:18:02 +0530
Subject: [PATCH 014/101] log chunks not remaining based on condition

---
 .../java/com/otaliastudios/transcoder/internal/audio/chunks.kt | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt
index 9ecddc21..c9bb6127 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt
@@ -26,7 +26,8 @@ class ChunkQueue(private val sampleRate: Int, private val channels: Int) {
     fun isEmpty() = queue.isEmpty()
 
     fun enqueue(buffer: ShortBuffer, timeUs: Long, timeStretch: Double, release: () -> Unit) {
-        Log.w("ChunkQueue", "enqueue: " + "buffer is empty")
+        if (!buffer.hasRemaining())
+            Log.w("ChunkQueue", "enqueue: " + "buffer is empty")
 //        require(buffer.hasRemaining())
 
         queue.addLast(Chunk(buffer, timeUs, timeStretch, release))

From 45d736c1de57b858fe9e5742b2e57d9a0199fc8a Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Wed, 9 Jun 2021 15:23:02 +0530
Subject: [PATCH 015/101] make conversion functions not internal

---
 .../otaliastudios/transcoder/internal/audio/conversions.kt    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/conversions.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/conversions.kt
index 6ca67e3f..0f0d2350 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/conversions.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/conversions.kt
@@ -15,12 +15,12 @@ internal fun bytesToUs(
     return MICROSECONDS_PER_SECOND * bytes / byteRate // usec
 }
 
-internal fun bitRate(sampleRate: Int, channels: Int): Int {
+ fun bitRate(sampleRate: Int, channels: Int): Int {
     val byteRate = channels * sampleRate * BYTES_PER_SAMPLE_PER_CHANNEL
     return byteRate * 8
 }
 
-internal fun samplesToBytes(samples: Int, channels: Int): Int {
+ fun samplesToBytes(samples: Int, channels: Int): Int {
     val bytesPerSample = BYTES_PER_SAMPLE_PER_CHANNEL * channels
     return samples * bytesPerSample
 }

From f977d6c018a44d036134574bcfc92cb3e798b59c Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Fri, 11 Jun 2021 13:47:46 +0530
Subject: [PATCH 016/101] expose outputmedia format

---
 .../main/java/com/otaliastudios/transcoder/Transcoder.java   | 4 +---
 .../java/com/otaliastudios/transcoder/TranscoderOptions.java | 5 +++--
 .../transcoder/internal/transcode/DefaultTranscodeEngine.kt  | 4 ++--
 .../transcoder/internal/transcode/TranscodeEngine.kt         | 3 ++-
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java b/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java
index a1b901a7..6fe4b722 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java
@@ -21,7 +21,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
-import com.otaliastudios.transcoder.common.TrackStatus;
 import com.otaliastudios.transcoder.common.TrackType;
 import com.otaliastudios.transcoder.internal.Codecs;
 import com.otaliastudios.transcoder.internal.pipeline.Pipeline;
@@ -33,7 +32,6 @@
 import java.io.FileDescriptor;
 import java.util.concurrent.Future;
 
-import kotlin.jvm.functions.Function3;
 import kotlin.jvm.functions.Function4;
 
 public class Transcoder {
@@ -103,7 +101,7 @@ public static TranscoderOptions.Builder into(@NonNull DataSink dataSink) {
      * @return a Future that completes when transcoding is completed
      */
     @NonNull
-    public Future<Void> transcode(@NonNull final TranscoderOptions options, Function3<? super TrackType, ? super DataSink, ? super Codecs, Pipeline> function) {
+    public Future<Void> transcode(@NonNull final TranscoderOptions options, Function4<? super TrackType, ? super DataSink, ? super Codecs, ? super MediaFormat, Pipeline> function) {
         return ThreadPool.getExecutor().submit(() -> {
             TranscodeEngine.transcode(options, function);
             return null;
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
index ba81677c..61b35225 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
@@ -1,6 +1,7 @@
 package com.otaliastudios.transcoder;
 
 import android.content.Context;
+import android.media.MediaFormat;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Handler;
@@ -37,7 +38,7 @@
 import java.util.List;
 import java.util.concurrent.Future;
 
-import kotlin.jvm.functions.Function3;
+import kotlin.jvm.functions.Function4;
 
 /**
  * Collects transcoding options consumed by {@link Transcoder}.
@@ -389,7 +390,7 @@ public TranscoderOptions build() {
         }
 
         @NonNull
-        public Future<Void> transcode(Function3<? super TrackType, ? super DataSink, ? super Codecs, Pipeline> function) {
+        public Future<Void> transcode(Function4<? super TrackType, ? super DataSink, ? super Codecs, ? super MediaFormat, Pipeline> function) {
             return Transcoder.getInstance().transcode(build(), function);
         }
     }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
index 0183bb8b..201bd4b1 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
@@ -33,7 +33,7 @@ class DefaultTranscodeEngine(
     private val audioStretcher: AudioStretcher,
     private val audioResampler: AudioResampler,
     interpolator: TimeInterpolator,
-    private val pipelineFactory: ((TrackType, DataSink, Codecs) -> Pipeline)?,
+    private val pipelineFactory: ((TrackType, DataSink, Codecs, MediaFormat) -> Pipeline)?,
     ) : TranscodeEngine() {
 
     private val log = Logger("TranscodeEngine")
@@ -77,7 +77,7 @@ class DefaultTranscodeEngine(
         }
         val sink = dataSink.ignoringEos { index < sources.lastIndex }
         if (pipelineFactory != null)
-            return pipelineFactory.invoke(type, sink, codecs)
+            return pipelineFactory.invoke(type, sink, codecs,outputFormat)
 
         return when (status) {
             TrackStatus.ABSENT -> EmptyPipeline()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
index da791a99..940b4469 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
@@ -1,5 +1,6 @@
 package com.otaliastudios.transcoder.internal.transcode
 
+import android.media.MediaFormat
 import com.otaliastudios.transcoder.Transcoder
 import com.otaliastudios.transcoder.TranscoderOptions
 import com.otaliastudios.transcoder.common.TrackType
@@ -30,7 +31,7 @@ abstract class TranscodeEngine {
         @JvmStatic
         fun transcode(
             options: TranscoderOptions,
-            function: ((TrackType, DataSink, Codecs) -> Pipeline)?,
+            function: ((TrackType, DataSink, Codecs, MediaFormat) -> Pipeline)?,
             ) {
             log.i("transcode(): called...")
             var engine: TranscodeEngine? = null

From 337720e344dcbaeddbe65e1e6ac3ff53780b5d24 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Sat, 12 Jun 2021 14:53:22 +0530
Subject: [PATCH 017/101] upsampling condition fix

---
 .../transcoder/resample/UpsampleAudioResampler.java             | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/resample/UpsampleAudioResampler.java b/lib/src/main/java/com/otaliastudios/transcoder/resample/UpsampleAudioResampler.java
index 69fb6251..f88b6bcc 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/resample/UpsampleAudioResampler.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/resample/UpsampleAudioResampler.java
@@ -28,7 +28,7 @@ public void resample(@NonNull ShortBuffer inputBuffer, int inputSampleRate, @Non
         int remainingFakeSamples = fakeSamples;
         float remainingInputSamplesRatio = ratio(remainingInputSamples, inputSamples);
         float remainingFakeSamplesRatio = ratio(remainingFakeSamples, fakeSamples);
-        while (remainingInputSamples > 0 && remainingFakeSamples > 0) {
+        while (remainingInputSamples > 0 && remainingFakeSamples >= 0) {
             // Will this be an input sample or a fake sample?
             // Choose the one with the bigger ratio.
             if (remainingInputSamplesRatio >= remainingFakeSamplesRatio) {

From 42d3da87963ceac031bacc8d3f2480ddbbc6748d Mon Sep 17 00:00:00 2001
From: Sahil Bajaj <sahilbajaj.nc@gmail.com>
Date: Fri, 18 Jun 2021 16:15:12 +0530
Subject: [PATCH 018/101] Set eos frame explicity to 0 bytes

---
 .../com/otaliastudios/transcoder/internal/codec/Decoder.kt    | 2 ++
 .../com/otaliastudios/transcoder/internal/codec/Encoder.kt    | 4 ++++
 2 files changed, 6 insertions(+)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index ce635842..bac47d4c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -110,6 +110,8 @@ class Decoder(
                 if (timeUs != null /* && (isEos || info.size > 0) */) {
                     dequeuedOutputs++
                     val buffer = buffers.getOutputBuffer(result)
+                    // Ideally, we shouldn't rely on the fact that the buffer is properly configured.
+                    // We should configure its position and limit based on the buffer info's position and size.
                     val data = DecoderData(buffer, timeUs) {
                         codec.releaseOutputBuffer(result, it)
                         dequeuedOutputs--
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt
index 274b07bd..8f4dfef4 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt
@@ -141,6 +141,10 @@ class Encoder(
                     val flags = info.flags and BUFFER_FLAG_END_OF_STREAM.inv()
                     val buffer = buffers.getOutputBuffer(result)
                     val timeUs = info.presentationTimeUs
+                    if (isEos && timeUs == 0L) {
+                        info.offset = 0
+                        info.size = 0
+                    }
                     buffer.clear()
                     buffer.limit(info.offset + info.size)
                     buffer.position(info.offset)

From ef8638944bf3e3a50570c933ac12ac2a2df989ec Mon Sep 17 00:00:00 2001
From: Sahil Bajaj <sahilbajaj.nc@gmail.com>
Date: Fri, 18 Jun 2021 16:15:39 +0530
Subject: [PATCH 019/101] Add logs in AudioEngine

---
 .../com/otaliastudios/transcoder/internal/audio/AudioEngine.kt | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
index d851d9d1..f2169097 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
@@ -56,6 +56,7 @@ class AudioEngine(
 
     override fun enqueue(data: DecoderData) {
         val stretch = (data as? DecoderTimerData)?.timeStretch ?: 1.0
+        log.v("Enqueue: ${data.timeUs}   $this")
         chunks.enqueue(data.buffer.asShortBuffer(), data.timeUs, stretch) {
             data.release(false)
         }
@@ -112,6 +113,8 @@ class AudioEngine(
             outBytes.clear()
             outBytes.limit(outBuffer.limit() * BYTES_PER_SHORT)
             outBytes.position(outBuffer.position() * BYTES_PER_SHORT)
+
+            log.v("Drain: ts: ${timeUs}  size: ${outBuffer.limit()}")
             State.Ok(EncoderData(outBytes, outId, timeUs))
         }
     }

From 8e2e260a14f3411ebf0cf2e782a9219197cf0169 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Wed, 23 Jun 2021 14:53:09 +0530
Subject: [PATCH 020/101] generate thumbnails in batch

---
 lib/build.gradle.kts                          |   1 +
 .../otaliastudios/transcoder/Thumbnailer.kt   |  47 ++++----
 .../transcoder/ThumbnailerOptions.kt          |   5 +-
 .../transcoder/internal/data/Reader.kt        |   2 +-
 .../transcoder/internal/data/Seeker.kt        |  14 ++-
 .../thumbnails/DefaultThumbnailsEngine.kt     | 104 ++++++++++--------
 .../internal/thumbnails/ThumbnailsEngine.kt   |  65 ++++++-----
 .../internal/video/VideoSnapshots.kt          |  11 +-
 8 files changed, 141 insertions(+), 108 deletions(-)

diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index a670c667..40c653ba 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -25,6 +25,7 @@ android {
 dependencies {
     api("com.otaliastudios.opengl:egloo:0.6.0")
     api("androidx.annotation:annotation:1.1.0")
+    api( "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
 
     androidTestImplementation("androidx.test:runner:1.3.0")
     androidTestImplementation("androidx.test:rules:1.3.0")
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt b/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt
index c519e49a..3f2b3f75 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt
@@ -1,26 +1,25 @@
 package com.otaliastudios.transcoder
 
-import com.otaliastudios.transcoder.internal.thumbnails.ThumbnailsEngine
-import com.otaliastudios.transcoder.internal.utils.ThreadPool
-import com.otaliastudios.transcoder.thumbnail.Thumbnail
-import java.util.concurrent.Callable
-import java.util.concurrent.Future
-
-class Thumbnailer private constructor() {
-
-    fun thumbnails(options: ThumbnailerOptions): Future<Void> {
-        return ThreadPool.executor.submit(Callable {
-            ThumbnailsEngine.thumbnails(options)
-            null
-        })
-    }
-
-    fun thumbnails(builder: ThumbnailerOptions.Builder.() -> Unit) = thumbnails(
-            options = ThumbnailerOptions.Builder().apply(builder).build()
-    )
-
-    companion object {
-        // Just for consistency with Transcoder class.
-        fun getInstance() = Thumbnailer()
-    }
-}
\ No newline at end of file
+//import com.otaliastudios.transcoder.internal.thumbnails.ThumbnailsEngine
+//import com.otaliastudios.transcoder.internal.utils.ThreadPool
+//import java.util.concurrent.Callable
+//import java.util.concurrent.Future
+//
+//class Thumbnailer private constructor() {
+//
+//    fun thumbnails(options: ThumbnailerOptions): Future<Void> {
+//        return ThreadPool.executor.submit(Callable {
+//            ThumbnailsEngine.thumbnails(options)
+//            null
+//        })
+//    }
+//
+//    fun thumbnails(builder: ThumbnailerOptions.Builder.() -> Unit) = thumbnails(
+//            options = ThumbnailerOptions.Builder().apply(builder).build()
+//    )
+//
+//    companion object {
+//        // Just for consistency with Transcoder class.
+//        fun getInstance() = Thumbnailer()
+//    }
+//}
\ No newline at end of file
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
index 5a0a35d6..38b315d5 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
@@ -4,6 +4,7 @@ import android.content.Context
 import android.net.Uri
 import android.os.Handler
 import android.os.Looper
+import com.otaliastudios.transcoder.internal.thumbnails.ThumbnailsEngine
 import com.otaliastudios.transcoder.resize.ExactResizer
 import com.otaliastudios.transcoder.resize.MultiResizer
 import com.otaliastudios.transcoder.resize.Resizer
@@ -104,8 +105,8 @@ class ThumbnailerOptions(
             )
         }
 
-        fun thumbnails(): Future<Void> {
-            return Thumbnailer.getInstance().thumbnails(build())
+        suspend fun thumbnails(): ThumbnailsEngine? {
+            return ThumbnailsEngine.thumbnails(build())
         }
     }
 }
\ No newline at end of file
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt
index 8e26fdac..7821cfb0 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt
@@ -35,7 +35,7 @@ class Reader(
     }
 
     override fun step(state: State.Ok<Unit>, fresh: Boolean): State<ReaderData> {
-        return if (source.isDrained) {
+        return if (source.isDrained || state is State.Eos) {
             log.i("Source is drained! Returning Eos as soon as possible.")
             nextBufferOrWait { byteBuffer, id ->
                 byteBuffer.limit(0)
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
index 4cf8c51e..c781d1e6 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
@@ -1,24 +1,30 @@
 package com.otaliastudios.transcoder.internal.data
 
-import com.otaliastudios.transcoder.common.TrackType
 import com.otaliastudios.transcoder.internal.pipeline.BaseStep
 import com.otaliastudios.transcoder.internal.pipeline.Channel
 import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.source.DataSource
-import java.nio.ByteBuffer
 
 class Seeker(
         private val source: DataSource,
-        positions: List<Long>,
+        private val fetchPositions:()-> List<Long>,
         private val seek: (Long) -> Boolean
 ) : BaseStep<Unit, Channel, Unit, Channel>() {
 
     private val log = Logger("Seeker")
     override val channel = Channel
-    private val positions = positions.toMutableList()
+
+    private val positions:MutableList<Long> = mutableListOf()
 
     override fun step(state: State.Ok<Unit>, fresh: Boolean): State<Unit> {
+        if(positions.isEmpty()) {
+            positions.addAll(fetchPositions())
+            if (positions.isEmpty()) {
+                return State.Eos(Unit)
+            }
+        }
+
         if (positions.isNotEmpty()) {
             if (seek(positions.first())) {
                 log.i("Seeking to next position ${positions.first()}")
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index df038fc5..693b3400 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -1,11 +1,6 @@
 package com.otaliastudios.transcoder.internal.thumbnails
 
-import android.graphics.Bitmap
 import android.media.MediaFormat
-import android.media.MediaFormat.KEY_HEIGHT
-import android.media.MediaFormat.KEY_WIDTH
-import android.opengl.GLES20
-import com.otaliastudios.opengl.core.Egloo
 import com.otaliastudios.transcoder.common.TrackStatus
 import com.otaliastudios.transcoder.common.TrackType
 import com.otaliastudios.transcoder.internal.DataSources
@@ -13,17 +8,13 @@ import com.otaliastudios.transcoder.internal.Segments
 import com.otaliastudios.transcoder.internal.Timer
 import com.otaliastudios.transcoder.internal.Tracks
 import com.otaliastudios.transcoder.internal.codec.Decoder
-import com.otaliastudios.transcoder.internal.codec.EncoderChannel
-import com.otaliastudios.transcoder.internal.codec.EncoderData
 import com.otaliastudios.transcoder.internal.data.Reader
 import com.otaliastudios.transcoder.internal.data.Seeker
-import com.otaliastudios.transcoder.internal.pipeline.*
-import com.otaliastudios.transcoder.internal.pipeline.Channel
-import com.otaliastudios.transcoder.internal.pipeline.EmptyPipeline
 import com.otaliastudios.transcoder.internal.pipeline.Pipeline
-import com.otaliastudios.transcoder.internal.utils.*
+import com.otaliastudios.transcoder.internal.pipeline.plus
+import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.internal.utils.forcingEos
-import com.otaliastudios.transcoder.internal.video.VideoPublisher
+import com.otaliastudios.transcoder.internal.utils.trackMapOf
 import com.otaliastudios.transcoder.internal.video.VideoRenderer
 import com.otaliastudios.transcoder.internal.video.VideoSnapshots
 import com.otaliastudios.transcoder.resize.Resizer
@@ -32,28 +23,28 @@ import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
 import com.otaliastudios.transcoder.thumbnail.ThumbnailRequest
 import com.otaliastudios.transcoder.time.DefaultTimeInterpolator
-import java.nio.ByteBuffer
-import java.nio.ByteOrder
-import kotlin.math.abs
 
 class DefaultThumbnailsEngine(
-        private val dataSources: DataSources,
-        private val rotation: Int,
-        resizer: Resizer,
-        requests: List<ThumbnailRequest>
+    private val dataSources: DataSources,
+    private val rotation: Int,
+    resizer: Resizer,
+    requests: List<ThumbnailRequest>
 ) : ThumbnailsEngine() {
 
+    private var finish = false
     private val log = Logger("ThumbnailsEngine")
 
     // Huge framerate triks the VideoRenderer into not dropping frames, which is important
     // for thumbnail requests that want to catch the very last frame.
-    private val tracks = Tracks(trackMapOf(
+    private val tracks = Tracks(
+        trackMapOf(
             video = DefaultVideoStrategy.Builder()
-                    .frameRate(120)
-                    .addResizer(resizer)
-                    .build(),
+                .frameRate(120)
+                .addResizer(resizer)
+                .build(),
             audio = RemoveTrackStrategy()
-    ), dataSources, rotation, true)
+        ), dataSources, rotation, true
+    )
 
     private val segments = Segments(dataSources, tracks, ::createPipeline)
 
@@ -63,43 +54,38 @@ class DefaultThumbnailsEngine(
         log.i("Created Tracks, Segments, Timer...")
     }
 
-    private val positions = requests.flatMap { request ->
-        val duration = timer.totalDurationUs
-        request.locate(duration).map { it to request }
-    }.sortedBy { it.first }
-
     private class Stub(
-            val request: ThumbnailRequest,
-            val positionUs: Long,
-            val localizedUs: Long) {
+        val request: ThumbnailRequest,
+        val positionUs: Long,
+        val localizedUs: Long
+    ) {
         var actualLocalizedUs: Long = localizedUs
     }
 
+    private var stubs = mutableListOf<Stub>()
+    private lateinit var type: TrackType
+    private var index: Int = 0
     private fun createPipeline(
         type: TrackType,
         index: Int,
         status: TrackStatus,
         outputFormat: MediaFormat
     ): Pipeline {
-        log.i("Creating pipeline #$index. absoluteUs=${positions.joinToString { it.first.toString() }}")
-        val stubs = positions.mapNotNull { (positionUs, request) ->
-            val localizedUs = timer.localize(type, index, positionUs)
-            localizedUs?.let { Stub(request, positionUs, localizedUs) }
-        }.toMutableList()
+        this.type = type
+        this.index = index
 
-        if (stubs.isEmpty()) return EmptyPipeline()
+//        if (stubs.isEmpty()) return EmptyPipeline()
         val source = dataSources[type][index].forcingEos {
-            stubs.isEmpty()
+            stubs.isEmpty() || finish
         }
-        val positions = stubs.map { it.localizedUs }
-        log.i("Requests for step #$index: ${positions.joinToString()} [duration=${source.durationUs}]")
         return Pipeline.build("Thumbnails") {
-            Seeker(source, positions) { it == stubs.firstOrNull()?.localizedUs } +
+            Seeker(source, fetchPositions) { it == stubs.firstOrNull()?.localizedUs } +
                     Reader(source, type) +
                     Decoder(source.getTrackFormat(type)!!, continuous = false) +
                     VideoRenderer(source.orientation, rotation, outputFormat, flipY = true) +
-                    VideoSnapshots(outputFormat, positions, 50 * 1000) { pos, bitmap ->
+                    VideoSnapshots(outputFormat, fetchPositions, 1000 * 1000) { pos, bitmap ->
                         val stub = stubs.removeFirst()
+                        timestamps.removeFirst()
                         stub.actualLocalizedUs = pos
                         log.i("Got snapshot. positionUs=${stub.positionUs} " +
                                 "localizedUs=${stub.localizedUs} " +
@@ -113,11 +99,14 @@ class DefaultThumbnailsEngine(
 
     private lateinit var progress: (Thumbnail) -> Unit
 
-    override fun thumbnails(progress: (Thumbnail) -> Unit) {
+    override fun queueThumbnails(list: List<ThumbnailRequest>, progress: (Thumbnail) -> Unit) {
+        segments.next(TrackType.VIDEO)
+        this.updatePositions(list)
         this.progress = progress
         while (true) {
             val advanced = segments.next(TrackType.VIDEO)?.advance() ?: false
-            val completed = !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
+            val completed =
+                !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
             if (Thread.interrupted()) {
                 throw InterruptedException()
             } else if (completed) {
@@ -128,6 +117,31 @@ class DefaultThumbnailsEngine(
         }
     }
 
+    fun finish() {
+        this.finish = true
+    }
+
+    private fun updatePositions(requests: List<ThumbnailRequest>) {
+        val positions = requests.flatMap { request ->
+            val duration = timer.totalDurationUs
+            request.locate(duration).map { it to request }
+        }.sortedBy { it.first }
+        log.i("Creating pipeline #$index. absoluteUs=${positions.joinToString { it.first.toString() }}")
+
+        stubs = positions.mapNotNull { (positionUs, request) ->
+            val localizedUs = timer.localize(type, index, positionUs)
+            localizedUs?.let { Stub(request, positionUs, localizedUs) }
+        }.toMutableList()
+
+        timestamps = stubs.map { it.localizedUs } as MutableList<Long>
+
+    }
+
+    private var timestamps = mutableListOf<Long>()
+    private val fetchPositions: () -> List<Long> = {
+        timestamps
+    }
+
     override fun cleanup() {
         runCatching { segments.release() }
         runCatching { dataSources.release() }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index 7ba13e4d..62458331 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -1,16 +1,14 @@
 package com.otaliastudios.transcoder.internal.thumbnails
 
 import com.otaliastudios.transcoder.ThumbnailerOptions
-import com.otaliastudios.transcoder.Transcoder
-import com.otaliastudios.transcoder.TranscoderOptions
 import com.otaliastudios.transcoder.internal.DataSources
 import com.otaliastudios.transcoder.internal.utils.Logger
-import com.otaliastudios.transcoder.internal.utils.trackMapOf
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
+import com.otaliastudios.transcoder.thumbnail.ThumbnailRequest
 
 abstract class ThumbnailsEngine {
 
-    abstract fun thumbnails(progress: (Thumbnail) -> Unit)
+    abstract fun queueThumbnails(list: List<ThumbnailRequest>, progress: (Thumbnail) -> Unit)
 
     abstract fun cleanup()
 
@@ -23,34 +21,43 @@ abstract class ThumbnailsEngine {
             return this.cause?.isInterrupted() ?: false
         }
 
+        var engine: ThumbnailsEngine? = null
+        private lateinit var dispatcher: ThumbnailsDispatcher
+
         @JvmStatic
-        fun thumbnails(options: ThumbnailerOptions) {
+        fun thumbnails(options: ThumbnailerOptions): ThumbnailsEngine? {
             log.i("thumbnails(): called...")
-            var engine: ThumbnailsEngine? = null
-            val dispatcher = ThumbnailsDispatcher(options)
-            try {
-                engine = DefaultThumbnailsEngine(
-                        dataSources = DataSources(options),
-                        rotation = options.rotation,
-                        resizer = options.resizer,
-                        requests = options.thumbnailRequests
-                )
-                engine.thumbnails {
-                    dispatcher.dispatchThumbnail(it)
-                }
-                dispatcher.dispatchCompletion()
-            } catch (e: Exception) {
-                if (e.isInterrupted()) {
-                    log.i("Transcode canceled.", e)
-                    dispatcher.dispatchCancel()
-                } else {
-                    log.e("Unexpected error while transcoding.", e)
-                    dispatcher.dispatchFailure(e)
-                    throw e
-                }
-            } finally {
-                engine?.cleanup()
+            dispatcher = ThumbnailsDispatcher(options)
+
+             engine = DefaultThumbnailsEngine(
+                dataSources = DataSources(options),
+                rotation = options.rotation,
+                resizer = options.resizer,
+                requests = options.thumbnailRequests
+            )
+            return engine
+        }
+    }
+    fun queue(list: List<ThumbnailRequest>){
+        engine?.queueThumbnails(list) {
+            dispatcher.dispatchThumbnail(it)
+        }
+
+        try {
+            dispatcher.dispatchCompletion()
+        } catch (e: Exception) {
+            if (e.isInterrupted()) {
+                log.i("Transcode canceled.", e)
+                dispatcher.dispatchCancel()
+            } else {
+                log.e("Unexpected error while transcoding.", e)
+                dispatcher.dispatchFailure(e)
+                throw e
             }
+        } finally {
+            engine?.cleanup()
         }
+
+
     }
 }
\ No newline at end of file
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt
index e3d45ae0..2793331e 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt
@@ -21,14 +21,14 @@ import kotlin.math.abs
 
 class VideoSnapshots(
         format: MediaFormat,
-        requests: List<Long>,
+        private val fetchRequests:()-> List<Long>,
         private val accuracyUs: Long,
         private val onSnapshot: (Long, Bitmap) -> Unit
 ) : BaseStep<Long, Channel, Long, Channel>() {
 
     private val log = Logger("VideoSnapshots")
     override val channel = Channel
-    private val requests = requests.toMutableList()
+    private val requests = mutableListOf<Long>()
     private val width = format.getInteger(KEY_WIDTH)
     private val height = format.getInteger(KEY_HEIGHT)
     private val core = EglCore(EGL14.EGL_NO_CONTEXT, EglCore.FLAG_RECORDABLE)
@@ -37,7 +37,12 @@ class VideoSnapshots(
     }
 
     override fun step(state: State.Ok<Long>, fresh: Boolean): State<Long> {
-        if (requests.isEmpty()) return state
+        if (requests.isEmpty()){
+            requests.addAll(fetchRequests())
+            if (requests.isEmpty()) {
+                return state
+            }
+        }
 
         val expectedUs = requests.first()
         val deltaUs = abs(expectedUs - state.value)

From 256aa76db754631ff585e87c395d5e432226c203 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Wed, 23 Jun 2021 15:05:04 +0530
Subject: [PATCH 021/101] global parameter usage removals

---
 .../thumbnails/DefaultThumbnailsEngine.kt     | 19 ++++++-------------
 1 file changed, 6 insertions(+), 13 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 693b3400..26f2ee4c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -63,16 +63,13 @@ class DefaultThumbnailsEngine(
     }
 
     private var stubs = mutableListOf<Stub>()
-    private lateinit var type: TrackType
-    private var index: Int = 0
+
     private fun createPipeline(
         type: TrackType,
         index: Int,
         status: TrackStatus,
         outputFormat: MediaFormat
     ): Pipeline {
-        this.type = type
-        this.index = index
 
 //        if (stubs.isEmpty()) return EmptyPipeline()
         val source = dataSources[type][index].forcingEos {
@@ -85,7 +82,6 @@ class DefaultThumbnailsEngine(
                     VideoRenderer(source.orientation, rotation, outputFormat, flipY = true) +
                     VideoSnapshots(outputFormat, fetchPositions, 1000 * 1000) { pos, bitmap ->
                         val stub = stubs.removeFirst()
-                        timestamps.removeFirst()
                         stub.actualLocalizedUs = pos
                         log.i("Got snapshot. positionUs=${stub.positionUs} " +
                                 "localizedUs=${stub.localizedUs} " +
@@ -100,8 +96,8 @@ class DefaultThumbnailsEngine(
     private lateinit var progress: (Thumbnail) -> Unit
 
     override fun queueThumbnails(list: List<ThumbnailRequest>, progress: (Thumbnail) -> Unit) {
-        segments.next(TrackType.VIDEO)
-        this.updatePositions(list)
+        val segment = segments.next(TrackType.VIDEO)
+        segment?.let { this.updatePositions(list, it.index) }
         this.progress = progress
         while (true) {
             val advanced = segments.next(TrackType.VIDEO)?.advance() ?: false
@@ -121,7 +117,7 @@ class DefaultThumbnailsEngine(
         this.finish = true
     }
 
-    private fun updatePositions(requests: List<ThumbnailRequest>) {
+    private fun updatePositions(requests: List<ThumbnailRequest>, index: Int) {
         val positions = requests.flatMap { request ->
             val duration = timer.totalDurationUs
             request.locate(duration).map { it to request }
@@ -129,17 +125,14 @@ class DefaultThumbnailsEngine(
         log.i("Creating pipeline #$index. absoluteUs=${positions.joinToString { it.first.toString() }}")
 
         stubs = positions.mapNotNull { (positionUs, request) ->
-            val localizedUs = timer.localize(type, index, positionUs)
+            val localizedUs = timer.localize(TrackType.VIDEO, index, positionUs)
             localizedUs?.let { Stub(request, positionUs, localizedUs) }
         }.toMutableList()
 
-        timestamps = stubs.map { it.localizedUs } as MutableList<Long>
-
     }
 
-    private var timestamps = mutableListOf<Long>()
     private val fetchPositions: () -> List<Long> = {
-        timestamps
+        stubs.map { it.localizedUs }
     }
 
     override fun cleanup() {

From 2841218f609f82681ea0a70ba28fbdd5b89f1dec Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Wed, 30 Jun 2021 19:34:55 +0530
Subject: [PATCH 022/101] dynamic thumbnails

---
 .../transcoder/ThumbnailerOptions.kt          |  2 +-
 .../transcoder/internal/Segments.kt           | 15 ++++++----
 .../transcoder/internal/data/Seeker.kt        | 12 ++++----
 .../thumbnails/DefaultThumbnailsEngine.kt     | 30 ++++++++++++-------
 4 files changed, 35 insertions(+), 24 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
index 38b315d5..bebcfe5b 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
@@ -105,7 +105,7 @@ class ThumbnailerOptions(
             )
         }
 
-        suspend fun thumbnails(): ThumbnailsEngine? {
+         fun thumbnails(): ThumbnailsEngine? {
             return ThumbnailsEngine.thumbnails(build())
         }
     }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
index 10e0e350..b009863f 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
@@ -8,9 +8,10 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.internal.utils.mutableTrackMapOf
 
 class Segments(
-        private val sources: DataSources,
-        private val tracks: Tracks,
-        private val factory: (TrackType, Int, TrackStatus, MediaFormat) -> Pipeline
+    private val sources: DataSources,
+    private val tracks: Tracks,
+    private val factory: (TrackType, Int, TrackStatus, MediaFormat) -> Pipeline,
+    private val ownsLifeCycle: Boolean = true
 ) {
 
     private val log = Logger("Segments")
@@ -49,8 +50,12 @@ class Segments(
             // create a new one. destroySegment will increase the requested index,
             // so if this is the last one, we'll return null.
             else -> {
-                destroySegment(current[type])
-                next(type)
+                if (ownsLifeCycle) {
+                    destroySegment(current[type])
+                    next(type)
+                } else {
+                    null
+                }
             }
         }
     }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
index c781d1e6..ba0ab702 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
@@ -7,9 +7,10 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.source.DataSource
 
 class Seeker(
-        private val source: DataSource,
-        private val fetchPositions:()-> List<Long>,
-        private val seek: (Long) -> Boolean
+    private val source: DataSource,
+    private val fetchPositions: () ->  List<Long>,
+    private val shouldFetch:() -> Boolean,
+    private val seek: (Long) -> Boolean
 ) : BaseStep<Unit, Channel, Unit, Channel>() {
 
     private val log = Logger("Seeker")
@@ -18,11 +19,8 @@ class Seeker(
     private val positions:MutableList<Long> = mutableListOf()
 
     override fun step(state: State.Ok<Unit>, fresh: Boolean): State<Unit> {
-        if(positions.isEmpty()) {
+        if(positions.isEmpty() && shouldFetch()) {
             positions.addAll(fetchPositions())
-            if (positions.isEmpty()) {
-                return State.Eos(Unit)
-            }
         }
 
         if (positions.isNotEmpty()) {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 26f2ee4c..07f08417 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -31,6 +31,7 @@ class DefaultThumbnailsEngine(
     requests: List<ThumbnailRequest>
 ) : ThumbnailsEngine() {
 
+    private var fetch: Boolean = false
     private var finish = false
     private val log = Logger("ThumbnailsEngine")
 
@@ -46,7 +47,7 @@ class DefaultThumbnailsEngine(
         ), dataSources, rotation, true
     )
 
-    private val segments = Segments(dataSources, tracks, ::createPipeline)
+    private val segments = Segments(dataSources, tracks, ::createPipeline, false)
 
     private val timer = Timer(DefaultTimeInterpolator(), dataSources, tracks, segments.currentIndex)
 
@@ -73,14 +74,14 @@ class DefaultThumbnailsEngine(
 
 //        if (stubs.isEmpty()) return EmptyPipeline()
         val source = dataSources[type][index].forcingEos {
-            stubs.isEmpty() || finish
+            stubs.isEmpty() && finish
         }
         return Pipeline.build("Thumbnails") {
-            Seeker(source, fetchPositions) { it == stubs.firstOrNull()?.localizedUs } +
+            Seeker(source, fetchPositions, shouldFetch) { it == stubs.firstOrNull()?.localizedUs } +
                     Reader(source, type) +
                     Decoder(source.getTrackFormat(type)!!, continuous = false) +
                     VideoRenderer(source.orientation, rotation, outputFormat, flipY = true) +
-                    VideoSnapshots(outputFormat, fetchPositions, 1000 * 1000) { pos, bitmap ->
+                    VideoSnapshots(outputFormat, fetchPositions, 300 * 1000) { pos, bitmap ->
                         val stub = stubs.removeFirst()
                         stub.actualLocalizedUs = pos
                         log.i("Got snapshot. positionUs=${stub.positionUs} " +
@@ -97,15 +98,16 @@ class DefaultThumbnailsEngine(
 
     override fun queueThumbnails(list: List<ThumbnailRequest>, progress: (Thumbnail) -> Unit) {
         val segment = segments.next(TrackType.VIDEO)
-        segment?.let { this.updatePositions(list, it.index) }
+        segment?.let {
+            this.updatePositions(list, it.index)
+        }
         this.progress = progress
         while (true) {
             val advanced = segments.next(TrackType.VIDEO)?.advance() ?: false
-            val completed =
-                !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
+            val completed = !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
             if (Thread.interrupted()) {
                 throw InterruptedException()
-            } else if (completed) {
+            } else if (completed || stubs.isEmpty()) {
                 break
             } else if (!advanced) {
                 Thread.sleep(WAIT_MS)
@@ -115,6 +117,7 @@ class DefaultThumbnailsEngine(
 
     fun finish() {
         this.finish = true
+        segments.release()
     }
 
     private fun updatePositions(requests: List<ThumbnailRequest>, index: Int) {
@@ -124,13 +127,18 @@ class DefaultThumbnailsEngine(
         }.sortedBy { it.first }
         log.i("Creating pipeline #$index. absoluteUs=${positions.joinToString { it.first.toString() }}")
 
-        stubs = positions.mapNotNull { (positionUs, request) ->
+        stubs.addAll(positions.mapNotNull { (positionUs, request) ->
             val localizedUs = timer.localize(TrackType.VIDEO, index, positionUs)
             localizedUs?.let { Stub(request, positionUs, localizedUs) }
-        }.toMutableList()
-
+        }.toMutableList())
+        fetch = true
     }
 
+    private val shouldFetch:() -> Boolean = {
+        fetch.also {
+            fetch = false
+        }
+    }
     private val fetchPositions: () -> List<Long> = {
         stubs.map { it.localizedUs }
     }

From c96c2bbe860b5864ada2b3dc0c233e0e42532be0 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Fri, 2 Jul 2021 15:54:07 +0530
Subject: [PATCH 023/101] fix eos

---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt        | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 07f08417..6a1a9740 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -72,10 +72,10 @@ class DefaultThumbnailsEngine(
         outputFormat: MediaFormat
     ): Pipeline {
 
-//        if (stubs.isEmpty()) return EmptyPipeline()
-        val source = dataSources[type][index].forcingEos {
-            stubs.isEmpty() && finish
-        }
+        val source = dataSources[type][index]
+//            .forcingEos {
+//            stubs.isEmpty() && finish
+//        }
         return Pipeline.build("Thumbnails") {
             Seeker(source, fetchPositions, shouldFetch) { it == stubs.firstOrNull()?.localizedUs } +
                     Reader(source, type) +

From c091a276bcf8886e172ebc0d4f4f6f09ab9a397a Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Mon, 5 Jul 2021 15:40:46 +0530
Subject: [PATCH 024/101] frame dropping conditions based on thumbnailer vs
 transcoding

---
 .../transcoder/internal/codec/DecoderDropper.kt              | 4 ++--
 .../internal/thumbnails/DefaultThumbnailsEngine.kt           | 5 ++---
 .../otaliastudios/transcoder/internal/video/VideoRenderer.kt | 5 +++--
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
index 4b5a38d0..36711b36 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
@@ -32,11 +32,11 @@ class DecoderDropper(private val continuous: Boolean) {
     private var firstOutputUs: Long? = null
 
     private fun debug(message: String, important: Boolean = false) {
-        /* val full = "$message firstInputUs=$firstInputUs " +
+         val full = "$message firstInputUs=$firstInputUs " +
                 "validInputUs=[${closedRanges.joinToString {
                     "$it(deltaUs=${closedDeltas[it]})"
                 }}] pendingRangeUs=${pendingRange}"
-        if (important) log.w(full) else log.v(full) */
+        if (important) log.w(full) else log.v(full)
     }
 
     fun input(timeUs: Long, render: Boolean) {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 6a1a9740..ea8a017b 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -13,7 +13,6 @@ import com.otaliastudios.transcoder.internal.data.Seeker
 import com.otaliastudios.transcoder.internal.pipeline.Pipeline
 import com.otaliastudios.transcoder.internal.pipeline.plus
 import com.otaliastudios.transcoder.internal.utils.Logger
-import com.otaliastudios.transcoder.internal.utils.forcingEos
 import com.otaliastudios.transcoder.internal.utils.trackMapOf
 import com.otaliastudios.transcoder.internal.video.VideoRenderer
 import com.otaliastudios.transcoder.internal.video.VideoSnapshots
@@ -80,8 +79,8 @@ class DefaultThumbnailsEngine(
             Seeker(source, fetchPositions, shouldFetch) { it == stubs.firstOrNull()?.localizedUs } +
                     Reader(source, type) +
                     Decoder(source.getTrackFormat(type)!!, continuous = false) +
-                    VideoRenderer(source.orientation, rotation, outputFormat, flipY = true) +
-                    VideoSnapshots(outputFormat, fetchPositions, 300 * 1000) { pos, bitmap ->
+                    VideoRenderer(source.orientation, rotation, outputFormat, flipY = true, true) +
+                    VideoSnapshots(outputFormat, fetchPositions, 500 * 1000) { pos, bitmap ->
                         val stub = stubs.removeFirst()
                         stub.actualLocalizedUs = pos
                         log.i("Got snapshot. positionUs=${stub.positionUs} " +
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
index d782db80..12d5ac0d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
@@ -16,7 +16,8 @@ class VideoRenderer(
         private val sourceRotation: Int, // intrinsic source rotation
         private val extraRotation: Int, // any extra rotation in TranscoderOptions
         private val targetFormat: MediaFormat,
-        flipY: Boolean = false
+        flipY: Boolean = false,
+        private val isThumbnailer:Boolean = false
 ): Step<DecoderData, DecoderChannel, Long, Channel>, DecoderChannel {
 
     private val log = Logger("VideoRenderer")
@@ -96,7 +97,7 @@ class VideoRenderer(
             state.value.release(false)
             State.Eos(0L)
         } else {
-            if (frameDropper.shouldRender(state.value.timeUs)) {
+            if (isThumbnailer || frameDropper.shouldRender(state.value.timeUs)) {
                 state.value.release(true)
                 frameDrawer.drawFrame()
                 State.Ok(state.value.timeUs)

From c545feac268be87261cf0c5466cfdf8422cd5b36 Mon Sep 17 00:00:00 2001
From: Sahil Bajaj <sahilbajaj.nc@gmail.com>
Date: Fri, 9 Jul 2021 01:00:59 +0530
Subject: [PATCH 025/101] thumbnail generationa and caching working

---
 .../internal/pipeline/CustomPipeline.kt       | 63 +++++++++++++++++++
 .../transcoder/internal/pipeline/Pipeline.kt  |  2 +-
 .../thumbnails/DefaultThumbnailsEngine.kt     | 13 ++--
 .../internal/thumbnails/ThumbnailsEngine.kt   |  4 +-
 .../thumbnail/SingleThumbnailRequest.kt       |  2 +-
 5 files changed, 74 insertions(+), 10 deletions(-)
 create mode 100644 lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/CustomPipeline.kt

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/CustomPipeline.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/CustomPipeline.kt
new file mode 100644
index 00000000..41938967
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/CustomPipeline.kt
@@ -0,0 +1,63 @@
+package com.otaliastudios.transcoder.internal.pipeline
+
+import com.otaliastudios.transcoder.internal.codec.EncoderChannel
+import com.otaliastudios.transcoder.internal.codec.EncoderData
+import com.otaliastudios.transcoder.internal.utils.Logger
+
+class CustomPipeline private constructor(name: String, private val chain: List<AnyStep>) {
+
+    private val log = Logger("Pipeline($name)")
+    private var headState: State.Ok<Any> = State.Ok(Unit)
+    private var headIndex = 0
+
+    init {
+        chain.zipWithNext().reversed().forEach { (first, next) ->
+            first.initialize(next = next.channel)
+        }
+    }
+
+    // Returns Eos, Ok or Wait
+    fun execute(): State<EncoderData> {
+        log.v("execute(): starting. head=$headIndex steps=${chain.size} remaining=${chain.size - headIndex}")
+        val head = headIndex
+        var state = headState
+        chain.forEachIndexed { index, step ->
+            if (index < head) return@forEachIndexed
+            val fresh = head == 0 || index != head
+            state = executeStep(state, step, fresh) ?: run {
+                log.v("execute(): step ${step.name} (#$index/${chain.size}) is waiting. headState=$headState headIndex=$headIndex")
+                return State.Wait
+            }
+            // log.v("execute(): executed ${step.name} (#$index/${chain.size}). result=$state")
+            if (state is State.Eos) {
+                log.i("execute(): EOS from ${step.name} (#$index/${chain.size}).")
+                headState = state
+                headIndex = index + 1
+            }
+        }
+        return when {
+            chain.isEmpty() -> State.Eos(EncoderData.Empty)
+            state is State.Eos -> State.Eos(EncoderData.Empty)
+            else -> State.Ok(state.value as EncoderData)
+        }
+    }
+
+    fun release() {
+        chain.forEach { it.release() }
+    }
+
+    private fun executeStep(previous: State.Ok<Any>, step: AnyStep, fresh: Boolean): State.Ok<Any>? {
+        return when (val state = step.step(previous, fresh)) {
+            is State.Ok -> state
+            is State.Retry -> executeStep(previous, step, fresh = false)
+            is State.Wait -> null
+        }
+    }
+
+    companion object {
+        @Suppress("UNCHECKED_CAST")
+         fun build(name: String, builder: () -> Pipeline.Builder<*, EncoderChannel> = { Pipeline.Builder<EncoderData, EncoderChannel>() }): CustomPipeline {
+            return CustomPipeline(name, builder().steps as List<AnyStep>)
+        }
+    }
+}
\ No newline at end of file
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
index b5a64453..39a41766 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
@@ -3,7 +3,7 @@ package com.otaliastudios.transcoder.internal.pipeline
 import com.otaliastudios.transcoder.internal.utils.Logger
 
 
-private typealias AnyStep = Step<Any, Channel, Any, Channel>
+internal typealias AnyStep = Step<Any, Channel, Any, Channel>
 
 class Pipeline private constructor(name: String, private val chain: List<AnyStep>) {
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index ea8a017b..d47d0052 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -22,6 +22,9 @@ import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
 import com.otaliastudios.transcoder.thumbnail.ThumbnailRequest
 import com.otaliastudios.transcoder.time.DefaultTimeInterpolator
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
 
 class DefaultThumbnailsEngine(
     private val dataSources: DataSources,
@@ -95,21 +98,19 @@ class DefaultThumbnailsEngine(
 
     private lateinit var progress: (Thumbnail) -> Unit
 
-    override fun queueThumbnails(list: List<ThumbnailRequest>, progress: (Thumbnail) -> Unit) {
+    override suspend fun queueThumbnails(list: List<ThumbnailRequest>, progress: (Thumbnail) -> Unit) {
         val segment = segments.next(TrackType.VIDEO)
         segment?.let {
             this.updatePositions(list, it.index)
         }
         this.progress = progress
-        while (true) {
+        while (currentCoroutineContext().isActive) {
             val advanced = segments.next(TrackType.VIDEO)?.advance() ?: false
             val completed = !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
-            if (Thread.interrupted()) {
-                throw InterruptedException()
-            } else if (completed || stubs.isEmpty()) {
+            if (completed || stubs.isEmpty()) {
                 break
             } else if (!advanced) {
-                Thread.sleep(WAIT_MS)
+                delay(WAIT_MS)
             }
         }
     }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index 62458331..96d0b02d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -8,7 +8,7 @@ import com.otaliastudios.transcoder.thumbnail.ThumbnailRequest
 
 abstract class ThumbnailsEngine {
 
-    abstract fun queueThumbnails(list: List<ThumbnailRequest>, progress: (Thumbnail) -> Unit)
+    abstract suspend fun queueThumbnails(list: List<ThumbnailRequest>, progress: (Thumbnail) -> Unit)
 
     abstract fun cleanup()
 
@@ -38,7 +38,7 @@ abstract class ThumbnailsEngine {
             return engine
         }
     }
-    fun queue(list: List<ThumbnailRequest>){
+    suspend fun queue(list: List<ThumbnailRequest>){
         engine?.queueThumbnails(list) {
             dispatcher.dispatchThumbnail(it)
         }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
index 7e17a213..5962ca09 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
@@ -1,6 +1,6 @@
 package com.otaliastudios.transcoder.thumbnail
 
-class SingleThumbnailRequest(private val positionUs: Long) : ThumbnailRequest {
+open class SingleThumbnailRequest(private val positionUs: Long) : ThumbnailRequest {
     override fun locate(durationUs: Long): List<Long> {
         require(positionUs in 0L..durationUs) {
             "Thumbnail position is out of range. position=$positionUs range=${0L..durationUs}"

From 9fe385be7c5d8bf3123d7a91c0aaf7e3d3eae225 Mon Sep 17 00:00:00 2001
From: Sahil Bajaj <sahilbajaj.nc@gmail.com>
Date: Fri, 9 Jul 2021 16:27:32 +0530
Subject: [PATCH 026/101] Skip timestamps between keyframe and actual time

---
 .../thumbnails/DefaultThumbnailsEngine.kt       |  4 +++-
 .../transcoder/internal/video/VideoRenderer.kt  | 17 +++++++++++++++--
 2 files changed, 18 insertions(+), 3 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index d47d0052..f628c2a1 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -82,7 +82,9 @@ class DefaultThumbnailsEngine(
             Seeker(source, fetchPositions, shouldFetch) { it == stubs.firstOrNull()?.localizedUs } +
                     Reader(source, type) +
                     Decoder(source.getTrackFormat(type)!!, continuous = false) +
-                    VideoRenderer(source.orientation, rotation, outputFormat, flipY = true, true) +
+                    VideoRenderer(source.orientation, rotation, outputFormat, flipY = true, true) {
+                        stubs.firstOrNull()?.positionUs ?: -1
+                    } +
                     VideoSnapshots(outputFormat, fetchPositions, 500 * 1000) { pos, bitmap ->
                         val stub = stubs.removeFirst()
                         stub.actualLocalizedUs = pos
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
index 12d5ac0d..25910b44 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
@@ -1,5 +1,6 @@
 package com.otaliastudios.transcoder.internal.video
 
+import android.graphics.Bitmap
 import android.media.MediaFormat
 import android.media.MediaFormat.*
 import android.view.Surface
@@ -17,7 +18,9 @@ class VideoRenderer(
         private val extraRotation: Int, // any extra rotation in TranscoderOptions
         private val targetFormat: MediaFormat,
         flipY: Boolean = false,
-        private val isThumbnailer:Boolean = false
+        private val isThumbnailer: Boolean = false,
+        private val getTargetTs: (() -> Long)? = null
+
 ): Step<DecoderData, DecoderChannel, Long, Channel>, DecoderChannel {
 
     private val log = Logger("VideoRenderer")
@@ -97,7 +100,8 @@ class VideoRenderer(
             state.value.release(false)
             State.Eos(0L)
         } else {
-            if (isThumbnailer || frameDropper.shouldRender(state.value.timeUs)) {
+            if ((isThumbnailer && shouldRenderThumbnail(state.value.timeUs, getTargetTs?.invoke() ?: -1))
+                || frameDropper.shouldRender(state.value.timeUs)) {
                 state.value.release(true)
                 frameDrawer.drawFrame()
                 State.Ok(state.value.timeUs)
@@ -108,6 +112,15 @@ class VideoRenderer(
         }
     }
 
+    private fun shouldRenderThumbnail(timeUs: Long, target: Long): Boolean {
+        if (timeUs >= target) {
+            log.i("Renderer: Rendering $timeUs : target: $target")
+        } else {
+            log.i("Renderer: Skipping $timeUs : target: $target")
+        }
+        return timeUs >= target
+    }
+
     override fun release() {
         frameDrawer.release()
     }

From ab9b859c2948b272a49ea04f5903510c942ad5c4 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Tue, 13 Jul 2021 15:33:03 +0530
Subject: [PATCH 027/101] refactor WIP

---
 build.gradle.kts                              |   4 +-
 lib/build.gradle.kts                          |   3 +-
 .../otaliastudios/transcoder/Thumbnailer.kt   |  12 +-
 .../transcoder/ThumbnailerListener.kt         |   2 +-
 .../transcoder/ThumbnailerOptions.kt          |  52 ++++----
 .../transcoder/common/TrackType.kt            |   2 +-
 .../transcoder/internal/Codecs.kt             |  15 +--
 .../transcoder/internal/DataSources.kt        |   7 +-
 .../transcoder/internal/Segment.kt            |   8 +-
 .../transcoder/internal/Segments.kt           |  23 ++--
 .../transcoder/internal/Timer.kt              |  20 +--
 .../transcoder/internal/Tracks.kt             |  24 ++--
 .../transcoder/internal/audio/AudioEngine.kt  |  40 +++---
 .../transcoder/internal/audio/chunks.kt       |  17 +--
 .../transcoder/internal/audio/conversions.kt  |  14 ++-
 .../internal/audio/remix/AudioRemixer.kt      |   2 +-
 .../transcoder/internal/audio/shorts.kt       |   6 +-
 .../transcoder/internal/codec/Decoder.kt      |  30 +++--
 .../internal/codec/DecoderDropper.kt          |  14 ++-
 .../transcoder/internal/codec/DecoderTimer.kt |  22 ++--
 .../transcoder/internal/codec/Encoder.kt      |  41 +++---
 .../transcoder/internal/data/Bridge.kt        |   6 +-
 .../transcoder/internal/data/Reader.kt        |   7 +-
 .../transcoder/internal/data/ReaderTimer.kt   |   6 +-
 .../transcoder/internal/data/Seeker.kt        |  24 ++--
 .../transcoder/internal/data/Writer.kt        |  26 ++--
 .../internal/pipeline/CustomPipeline.kt       |  13 +-
 .../transcoder/internal/pipeline/Pipeline.kt  |  40 +++---
 .../transcoder/internal/pipeline/State.kt     |   2 +-
 .../transcoder/internal/pipeline/Step.kt      |  12 +-
 .../transcoder/internal/pipeline/pipelines.kt | 117 ++++++++----------
 .../transcoder/internal/pipeline/steps.kt     |  26 ++--
 .../thumbnails/DefaultThumbnailsEngine.kt     | 100 +++++++++------
 .../internal/thumbnails/ThumbnailsEngine.kt   |  23 ++--
 .../transcode/DefaultTranscodeEngine.kt       |  39 +++---
 .../internal/transcode/TranscodeEngine.kt     |  38 +++---
 .../transcoder/internal/utils/ThreadPool.kt   |  27 ++--
 .../transcoder/internal/utils/TrackMap.kt     |   1 -
 .../transcoder/internal/utils/debug.kt        |   4 +-
 .../transcoder/internal/utils/eos.kt          |  20 ++-
 .../transcoder/internal/video/FrameDropper.kt |  30 +++--
 .../internal/video/VideoPublisher.kt          |  15 +--
 .../internal/video/VideoRenderer.kt           |  45 ++++---
 .../internal/video/VideoSnapshots.kt          |  24 ++--
 .../thumbnail/CoverThumbnailRequest.kt        |   2 +-
 .../thumbnail/SingleThumbnailRequest.kt       |   2 +-
 .../transcoder/thumbnail/Thumbnail.kt         |   8 +-
 .../transcoder/thumbnail/ThumbnailRequest.kt  |   2 +-
 .../thumbnail/UniformThumbnailRequest.kt      |   2 +-
 49 files changed, 536 insertions(+), 483 deletions(-)

diff --git a/build.gradle.kts b/build.gradle.kts
index a4bb9799..c3f98fac 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,7 +3,7 @@ buildscript {
     extra["minSdkVersion"] = 18
     extra["compileSdkVersion"] = 30
     extra["targetSdkVersion"] = 30
-    
+
     repositories {
         google()
         mavenCentral()
@@ -28,4 +28,4 @@ allprojects {
 
 tasks.register("clean", Delete::class) {
     delete(buildDir)
-}
\ No newline at end of file
+}
diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index 40c653ba..72f8b017 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -21,11 +21,10 @@ android {
     buildTypes["release"].isMinifyEnabled = false
 }
 
-
 dependencies {
     api("com.otaliastudios.opengl:egloo:0.6.0")
     api("androidx.annotation:annotation:1.1.0")
-    api( "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
+    api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
 
     androidTestImplementation("androidx.test:runner:1.3.0")
     androidTestImplementation("androidx.test:rules:1.3.0")
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt b/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt
index 3f2b3f75..fce4929a 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt
@@ -1,11 +1,11 @@
 package com.otaliastudios.transcoder
 
-//import com.otaliastudios.transcoder.internal.thumbnails.ThumbnailsEngine
-//import com.otaliastudios.transcoder.internal.utils.ThreadPool
-//import java.util.concurrent.Callable
-//import java.util.concurrent.Future
+// import com.otaliastudios.transcoder.internal.thumbnails.ThumbnailsEngine
+// import com.otaliastudios.transcoder.internal.utils.ThreadPool
+// import java.util.concurrent.Callable
+// import java.util.concurrent.Future
 //
-//class Thumbnailer private constructor() {
+// class Thumbnailer private constructor() {
 //
 //    fun thumbnails(options: ThumbnailerOptions): Future<Void> {
 //        return ThreadPool.executor.submit(Callable {
@@ -22,4 +22,4 @@ package com.otaliastudios.transcoder
 //        // Just for consistency with Transcoder class.
 //        fun getInstance() = Thumbnailer()
 //    }
-//}
\ No newline at end of file
+// }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerListener.kt b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerListener.kt
index b0f89872..e79e4de5 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerListener.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerListener.kt
@@ -11,4 +11,4 @@ interface ThumbnailerListener {
     fun onThumbnailsCanceled() = Unit
 
     fun onThumbnailsFailed(exception: Throwable)
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
index bebcfe5b..2d74a303 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
@@ -1,3 +1,5 @@
+@file:Suppress("TooManyFunctions", "MagicNumber")
+
 package com.otaliastudios.transcoder
 
 import android.content.Context
@@ -14,16 +16,15 @@ import com.otaliastudios.transcoder.source.FilePathDataSource
 import com.otaliastudios.transcoder.source.UriDataSource
 import com.otaliastudios.transcoder.thumbnail.ThumbnailRequest
 import java.io.FileDescriptor
-import java.util.concurrent.Future
 
 @Suppress("unused")
 class ThumbnailerOptions(
-        val dataSources: List<DataSource>,
-        val resizer: Resizer,
-        val rotation: Int,
-        val thumbnailRequests: List<ThumbnailRequest>,
-        val listener: ThumbnailerListener,
-        val listenerHandler: Handler
+    val dataSources: List<DataSource>,
+    val resizer: Resizer,
+    val rotation: Int,
+    val thumbnailRequests: List<ThumbnailRequest>,
+    val listener: ThumbnailerListener?,
+    val listenerHandler: Handler
 ) {
 
     class Builder {
@@ -40,14 +41,14 @@ class ThumbnailerOptions(
             dataSources.add(dataSource)
         }
 
-        fun addDataSource(fileDescriptor: FileDescriptor)
-                = addDataSource(FileDescriptorDataSource(fileDescriptor))
+        fun addDataSource(fileDescriptor: FileDescriptor) =
+            addDataSource(FileDescriptorDataSource(fileDescriptor))
 
-        fun addDataSource(filePath: String)
-                = addDataSource(FilePathDataSource(filePath))
+        fun addDataSource(filePath: String) =
+            addDataSource(FilePathDataSource(filePath))
 
-        fun addDataSource(context: Context, uri: Uri)
-                = addDataSource(UriDataSource(context, uri))
+        fun addDataSource(context: Context, uri: Uri) =
+            addDataSource(UriDataSource(context, uri))
 
         /**
          * Sets the video output strategy. If absent, this defaults to 320x240 images.
@@ -86,27 +87,22 @@ class ThumbnailerOptions(
             require(dataSources.isNotEmpty()) {
                 "At least one data source is required!"
             }
-            require(thumbnailRequests.isNotEmpty()) {
-                "At least one thumbnail request is required!"
-            }
-            val listener = requireNotNull(listener) {
-                "Listener can't be null."
-            }
+
             val listenerHandler = listenerHandler
-                    ?: Handler(Looper.myLooper() ?: Looper.getMainLooper())
+                ?: Handler(Looper.myLooper() ?: Looper.getMainLooper())
             val resizer = if (resizerSet) resizer else ExactResizer(320, 240)
             return ThumbnailerOptions(
-                    dataSources = dataSources.toList(),
-                    resizer = resizer,
-                    rotation = rotation,
-                    thumbnailRequests = thumbnailRequests.toList(),
-                    listener = listener,
-                    listenerHandler = listenerHandler
+                dataSources = dataSources.toList(),
+                resizer = resizer,
+                rotation = rotation,
+                thumbnailRequests = thumbnailRequests.toList(),
+                listener = listener,
+                listenerHandler = listenerHandler
             )
         }
 
-         fun thumbnails(): ThumbnailsEngine? {
+        fun thumbnails(): ThumbnailsEngine? {
             return ThumbnailsEngine.thumbnails(build())
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/common/TrackType.kt b/lib/src/main/java/com/otaliastudios/transcoder/common/TrackType.kt
index 2dbbf7d1..10946eb0 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/common/TrackType.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/common/TrackType.kt
@@ -14,4 +14,4 @@ internal val MediaFormat.trackTypeOrNull get() = when {
     getString(MediaFormat.KEY_MIME)!!.startsWith("audio/") -> TrackType.AUDIO
     getString(MediaFormat.KEY_MIME)!!.startsWith("video/") -> TrackType.VIDEO
     else -> null
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
index bfc471b6..d0a01273 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
@@ -1,19 +1,12 @@
 package com.otaliastudios.transcoder.internal
 
 import android.media.MediaCodec
-import android.media.MediaCodecInfo
 import android.media.MediaFormat
-import android.os.Build
-import android.util.Log
 import android.view.Surface
 import com.otaliastudios.transcoder.common.TrackStatus
 import com.otaliastudios.transcoder.common.TrackType
-import com.otaliastudios.transcoder.internal.media.MediaFormatProvider
 import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.internal.utils.TrackMap
-import com.otaliastudios.transcoder.internal.utils.trackMapOf
-import com.otaliastudios.transcoder.source.DataSource
-import com.otaliastudios.transcoder.strategy.TrackStrategy
 
 /**
  * Encoders are shared between segments. This is not strictly needed but it is more efficient
@@ -23,9 +16,9 @@ import com.otaliastudios.transcoder.strategy.TrackStrategy
  * data to MediaMuxer / MPEG4Writer.
  */
 class Codecs(
-        private val sources: DataSources,
-        private val tracks: Tracks,
-        private val current: TrackMap<Int>
+    private val sources: DataSources,
+    private val tracks: Tracks,
+    private val current: TrackMap<Int>
 ) {
 
     private val log = Logger("Codecs")
@@ -70,4 +63,4 @@ class Codecs(
             it.first.release()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
index 85bab866..97caa630 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
@@ -5,13 +5,12 @@ import com.otaliastudios.transcoder.TranscoderOptions
 import com.otaliastudios.transcoder.common.TrackType
 import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.internal.utils.TrackMap
-import com.otaliastudios.transcoder.internal.utils.trackMapOf
 import com.otaliastudios.transcoder.source.BlankAudioDataSource
 import com.otaliastudios.transcoder.source.DataSource
 
 class DataSources constructor(
-        videoSources: List<DataSource>,
-        audioSources: List<DataSource>,
+    videoSources: List<DataSource>,
+    audioSources: List<DataSource>,
 ) : TrackMap<List<DataSource>> {
 
     constructor(options: TranscoderOptions) : this(options.videoDataSources, options.audioDataSources)
@@ -68,4 +67,4 @@ class DataSources constructor(
         audio.forEach { it.deinit() }
         log.i("release(): released.")
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt
index 807593fe..430b9d52 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt
@@ -6,9 +6,9 @@ import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.internal.utils.Logger
 
 class Segment(
-        val type: TrackType,
-        val index: Int,
-        private val pipeline: Pipeline,
+    val type: TrackType,
+    val index: Int,
+    private val pipeline: Pipeline,
 ) {
 
     private val log = Logger("Segment($type,$index)")
@@ -27,4 +27,4 @@ class Segment(
     fun release() {
         pipeline.release()
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
index b009863f..bbe96a01 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
@@ -1,3 +1,5 @@
+@file:Suppress("ReturnCount")
+
 package com.otaliastudios.transcoder.internal
 
 import android.media.MediaFormat
@@ -21,7 +23,10 @@ class Segments(
 
     fun hasNext(type: TrackType): Boolean {
         if (!sources.has(type)) return false
-        log.v("hasNext($type): segment=${current.getOrNull(type)} lastIndex=${sources.getOrNull(type)?.lastIndex} canAdvance=${current.getOrNull(type)?.canAdvance()}")
+        log.v(
+            "hasNext($type): segment=${current.getOrNull(type)} lastIndex=${sources.getOrNull(type)?.lastIndex}" +
+                " canAdvance=${current.getOrNull(type)?.canAdvance()}"
+        )
         val segment = current.getOrNull(type) ?: return true // not started
         val lastIndex = sources.getOrNull(type)?.lastIndex ?: return false // no track!
         return segment.canAdvance() || segment.index < lastIndex
@@ -89,15 +94,15 @@ class Segments(
         // who check it during pipeline init.
         currentIndex[type] = index
         val pipeline = factory(
-                type,
-                index,
-                tracks.all[type],
-                tracks.outputFormats[type]
+            type,
+            index,
+            tracks.all[type],
+            tracks.outputFormats[type]
         )
         return Segment(
-                type = type,
-                index = index,
-                pipeline = pipeline
+            type = type,
+            index = index,
+            pipeline = pipeline
         ).also {
             current[type] = it
         }
@@ -111,4 +116,4 @@ class Segments(
         }
         requestedIndex[segment.type] = segment.index + 1
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt
index 4ce662cc..307f3c5b 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt
@@ -1,3 +1,5 @@
+@file:Suppress("MagicNumber")
+
 package com.otaliastudios.transcoder.internal
 
 import com.otaliastudios.transcoder.common.TrackType
@@ -7,10 +9,10 @@ import com.otaliastudios.transcoder.source.DataSource
 import com.otaliastudios.transcoder.time.TimeInterpolator
 
 class Timer(
-        private val interpolator: TimeInterpolator,
-        private val sources: DataSources,
-        private val tracks: Tracks,
-        private val current: TrackMap<Int>
+    private val interpolator: TimeInterpolator,
+    private val sources: DataSources,
+    private val tracks: Tracks,
+    private val current: TrackMap<Int>
 ) {
 
     private val log = Logger("Timer")
@@ -60,12 +62,10 @@ class Timer(
     fun localize(type: TrackType, index: Int, positionUs: Long): Long? {
         if (!tracks.active.has(type)) return null
         val behindUs = sources[type]
-                .filterIndexed { i, _ -> i < index }
-                .durationUs(-1)
+            .filterIndexed { i, _ -> i < index }
+            .durationUs(-1)
         val localizedUs = positionUs - behindUs
-        if (localizedUs < 0L) return null
-        if (localizedUs > sources[type][index].durationUs) return null
-        return localizedUs
+        return if (localizedUs < 0L || localizedUs > sources[type][index].durationUs) null else localizedUs
     }
 
     fun interpolator(type: TrackType, index: Int) = interpolators.getOrPut(type to index) {
@@ -92,4 +92,4 @@ class Timer(
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt
index a099bb68..44fd586d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt
@@ -11,10 +11,10 @@ import com.otaliastudios.transcoder.source.DataSource
 import com.otaliastudios.transcoder.strategy.TrackStrategy
 
 class Tracks(
-        strategies: TrackMap<TrackStrategy>,
-        sources: DataSources,
-        videoRotation: Int,
-        forceCompression: Boolean
+    strategies: TrackMap<TrackStrategy>,
+    sources: DataSources,
+    videoRotation: Int,
+    forceCompression: Boolean
 ) {
 
     private val log = Logger("Tracks")
@@ -27,8 +27,8 @@ class Tracks(
         val (audioFormat, audioStatus) = resolveTrack(TrackType.AUDIO, strategies.audio, sources.audioOrNull())
         val (videoFormat, videoStatus) = resolveTrack(TrackType.VIDEO, strategies.video, sources.videoOrNull())
         all = trackMapOf(
-                video = resolveVideoStatus(videoStatus, forceCompression, videoRotation),
-                audio = resolveAudioStatus(audioStatus, forceCompression)
+            video = resolveVideoStatus(videoStatus, forceCompression, videoRotation),
+            audio = resolveAudioStatus(audioStatus, forceCompression)
         )
         outputFormats = trackMapOf(video = videoFormat, audio = audioFormat)
         log.i("init: videoStatus=$videoStatus, resolvedVideoStatus=${all.video}, videoFormat=$videoFormat")
@@ -36,8 +36,8 @@ class Tracks(
     }
 
     val active: TrackMap<TrackStatus> = trackMapOf(
-            video = all.video.takeIf { it.isTranscoding },
-            audio = all.audio.takeIf { it.isTranscoding }
+        video = all.video.takeIf { it.isTranscoding },
+        audio = all.audio.takeIf { it.isTranscoding }
     )
 
     private fun resolveVideoStatus(status: TrackStatus, forceCompression: Boolean, rotation: Int): TrackStatus {
@@ -53,9 +53,9 @@ class Tracks(
     }
 
     private fun resolveTrack(
-            type: TrackType,
-            strategy: TrackStrategy,
-            sources: List<DataSource>? // null or not-empty
+        type: TrackType,
+        strategy: TrackStrategy,
+        sources: List<DataSource>? // null or not-empty
     ): Pair<MediaFormat, TrackStatus> {
         if (sources == null) {
             return MediaFormat() to TrackStatus.ABSENT
@@ -79,4 +79,4 @@ class Tracks(
             else -> error("Of all $type sources, some have a $type track, some don't.")
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
index f2169097..928431c6 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
@@ -1,13 +1,20 @@
+@file:Suppress("ReturnCount")
+
 package com.otaliastudios.transcoder.internal.audio
 
 import android.media.MediaFormat
-import android.media.MediaFormat.*
+import android.media.MediaFormat.KEY_CHANNEL_COUNT
+import android.media.MediaFormat.KEY_SAMPLE_RATE
 import android.view.Surface
 import com.otaliastudios.transcoder.internal.audio.remix.AudioRemixer
-import com.otaliastudios.transcoder.internal.codec.*
-import com.otaliastudios.transcoder.internal.pipeline.*
+import com.otaliastudios.transcoder.internal.codec.DecoderChannel
+import com.otaliastudios.transcoder.internal.codec.DecoderData
+import com.otaliastudios.transcoder.internal.codec.DecoderTimerData
+import com.otaliastudios.transcoder.internal.codec.EncoderChannel
+import com.otaliastudios.transcoder.internal.codec.EncoderData
+import com.otaliastudios.transcoder.internal.pipeline.QueuedStep
+import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.internal.utils.Logger
-import com.otaliastudios.transcoder.internal.utils.trackMapOf
 import com.otaliastudios.transcoder.resample.AudioResampler
 import com.otaliastudios.transcoder.stretch.AudioStretcher
 import java.util.concurrent.atomic.AtomicInteger
@@ -16,13 +23,13 @@ import kotlin.math.floor
 
 /**
  * Performs audio rendering, from decoder output to encoder input, applying sample rate conversion,
- * remixing, stretching. TODO: With some extra work this could be split in different steps.
+ * remixing, stretching.
  */
 class AudioEngine(
-        private val stretcher: AudioStretcher,
-        private val resampler: AudioResampler,
-        private val targetFormat: MediaFormat
-): QueuedStep<DecoderData, DecoderChannel, EncoderData, EncoderChannel>(), DecoderChannel {
+    private val stretcher: AudioStretcher,
+    private val resampler: AudioResampler,
+    private val targetFormat: MediaFormat
+) : QueuedStep<DecoderData, DecoderChannel, EncoderData, EncoderChannel>(), DecoderChannel {
 
     companion object {
         private val ID = AtomicInteger(0)
@@ -73,7 +80,7 @@ class AudioEngine(
         }
         val outBuffer = outBytes.asShortBuffer()
         return chunks.drain(
-                eos = State.Eos(EncoderData(outBytes, outId, 0))
+            eos = State.Eos(EncoderData(outBytes, outId, 0))
         ) { inBuffer, timeUs, stretch ->
             val outSize = outBuffer.remaining()
             val inSize = inBuffer.remaining()
@@ -104,9 +111,12 @@ class AudioEngine(
 
             // Resample
             resampler.resample(
-                    remixBuffer, rawFormat.sampleRate,
-                    outBuffer, targetFormat.sampleRate,
-                    targetFormat.channels)
+                remixBuffer,
+                rawFormat.sampleRate,
+                outBuffer,
+                targetFormat.sampleRate,
+                targetFormat.channels
+            )
             outBuffer.flip()
 
             // Adjust position and dispatch.
@@ -114,8 +124,8 @@ class AudioEngine(
             outBytes.limit(outBuffer.limit() * BYTES_PER_SHORT)
             outBytes.position(outBuffer.position() * BYTES_PER_SHORT)
 
-            log.v("Drain: ts: ${timeUs}  size: ${outBuffer.limit()}")
+            log.v("Drain: ts: $timeUs  size: ${outBuffer.limit()}")
             State.Ok(EncoderData(outBytes, outId, timeUs))
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt
index c9bb6127..517a4b1c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/chunks.kt
@@ -4,10 +4,10 @@ import android.util.Log
 import java.nio.ShortBuffer
 
 private data class Chunk(
-        val buffer: ShortBuffer,
-        val timeUs: Long,
-        val timeStretch: Double,
-        val release: () -> Unit
+    val buffer: ShortBuffer,
+    val timeUs: Long,
+    val timeStretch: Double,
+    val release: () -> Unit
 ) {
     companion object {
         val Eos = Chunk(ShortBuffer.allocate(0), 0, 0.0, {})
@@ -26,8 +26,9 @@ class ChunkQueue(private val sampleRate: Int, private val channels: Int) {
     fun isEmpty() = queue.isEmpty()
 
     fun enqueue(buffer: ShortBuffer, timeUs: Long, timeStretch: Double, release: () -> Unit) {
-        if (!buffer.hasRemaining())
+        if (!buffer.hasRemaining()) {
             Log.w("ChunkQueue", "enqueue: " + "buffer is empty")
+        }
 //        require(buffer.hasRemaining())
 
         queue.addLast(Chunk(buffer, timeUs, timeStretch, release))
@@ -48,9 +49,11 @@ class ChunkQueue(private val sampleRate: Int, private val channels: Int) {
         head.buffer.limit(limit)
         if (head.buffer.hasRemaining()) {
             val consumed = size - head.buffer.remaining()
-            queue.addFirst(head.copy(
+            queue.addFirst(
+                head.copy(
                     timeUs = shortsToUs(consumed, sampleRate, channels)
-            ))
+                )
+            )
         } else {
             // buffer consumed!
             head.release()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/conversions.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/conversions.kt
index 0f0d2350..dd9e7f19 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/conversions.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/conversions.kt
@@ -1,3 +1,5 @@
+@file:Suppress("MagicNumber")
+
 package com.otaliastudios.transcoder.internal.audio
 
 import kotlin.math.ceil
@@ -6,21 +8,21 @@ private const val BYTES_PER_SAMPLE_PER_CHANNEL = 2 // Assuming 16bit audio, so 2
 private const val MICROSECONDS_PER_SECOND = 1000000L
 
 internal fun bytesToUs(
-        bytes: Int /* bytes */,
-        sampleRate: Int /* samples/sec */,
-        channels: Int /* channel */
+    bytes: Int /* bytes */,
+    sampleRate: Int /* samples/sec */,
+    channels: Int /* channel */
 ): Long {
     val byteRatePerChannel = sampleRate * BYTES_PER_SAMPLE_PER_CHANNEL // bytes/sec/channel
     val byteRate = byteRatePerChannel * channels // bytes/sec
     return MICROSECONDS_PER_SECOND * bytes / byteRate // usec
 }
 
- fun bitRate(sampleRate: Int, channels: Int): Int {
+fun bitRate(sampleRate: Int, channels: Int): Int {
     val byteRate = channels * sampleRate * BYTES_PER_SAMPLE_PER_CHANNEL
     return byteRate * 8
 }
 
- fun samplesToBytes(samples: Int, channels: Int): Int {
+fun samplesToBytes(samples: Int, channels: Int): Int {
     val bytesPerSample = BYTES_PER_SAMPLE_PER_CHANNEL * channels
     return samples * bytesPerSample
 }
@@ -37,4 +39,4 @@ internal fun shortsToUs(shorts: Int, sampleRate: Int, channels: Int): Long {
 
 internal fun usToShorts(us: Long, sampleRate: Int, channels: Int): Int {
     return usToBytes(us, sampleRate, channels) / BYTES_PER_SHORT
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/remix/AudioRemixer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/remix/AudioRemixer.kt
index 40b60845..3e4ccabf 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/remix/AudioRemixer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/remix/AudioRemixer.kt
@@ -30,4 +30,4 @@ interface AudioRemixer {
             else -> PassThroughAudioRemixer()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/shorts.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/shorts.kt
index 56ece6c2..2c237be5 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/shorts.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/shorts.kt
@@ -13,8 +13,8 @@ class ShortBuffers {
         var current = map[name]
         if (current == null || current.capacity() < size) {
             current = ByteBuffer.allocateDirect(size * BYTES_PER_SHORT)
-                    .order(ByteOrder.nativeOrder())
-                    .asShortBuffer()
+                .order(ByteOrder.nativeOrder())
+                .asShortBuffer()
         }
         current!!.clear()
         current.limit(size)
@@ -22,4 +22,4 @@ class ShortBuffers {
             map[name] = current
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index bac47d4c..ed4ca3e9 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -1,13 +1,18 @@
 package com.otaliastudios.transcoder.internal.codec
 
-import android.media.MediaCodec.*
+import android.media.MediaCodec
+import android.media.MediaCodec.BUFFER_FLAG_END_OF_STREAM
+import android.media.MediaCodec.BUFFER_FLAG_SYNC_FRAME
+import android.media.MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED
+import android.media.MediaCodec.INFO_OUTPUT_FORMAT_CHANGED
+import android.media.MediaCodec.INFO_TRY_AGAIN_LATER
+import android.media.MediaCodec.createDecoderByType
 import android.media.MediaFormat
 import android.view.Surface
 import com.otaliastudios.transcoder.common.trackType
 import com.otaliastudios.transcoder.internal.data.ReaderChannel
 import com.otaliastudios.transcoder.internal.data.ReaderData
 import com.otaliastudios.transcoder.internal.media.MediaCodecBuffers
-import com.otaliastudios.transcoder.internal.pipeline.BaseStep
 import com.otaliastudios.transcoder.internal.pipeline.Channel
 import com.otaliastudios.transcoder.internal.pipeline.QueuedStep
 import com.otaliastudios.transcoder.internal.pipeline.State
@@ -15,14 +20,12 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.internal.utils.trackMapOf
 import java.nio.ByteBuffer
 import java.util.concurrent.atomic.AtomicInteger
-import kotlin.properties.Delegates
 import kotlin.properties.Delegates.observable
 
-
 open class DecoderData(
-        val buffer: ByteBuffer,
-        val timeUs: Long,
-        val release: (render: Boolean) -> Unit
+    val buffer: ByteBuffer,
+    val timeUs: Long,
+    val release: (render: Boolean) -> Unit
 )
 
 interface DecoderChannel : Channel {
@@ -31,8 +34,8 @@ interface DecoderChannel : Channel {
 }
 
 class Decoder(
-        private val format: MediaFormat, // source.getTrackFormat(track)
-        continuous: Boolean, // relevant if the source sends no-render chunks. should we compensate or not?
+    private val format: MediaFormat, // source.getTrackFormat(track)
+    continuous: Boolean, // relevant if the source sends no-render chunks. should we compensate or not?
 ) : QueuedStep<ReaderData, ReaderChannel, DecoderData, DecoderChannel>(), ReaderChannel {
 
     companion object {
@@ -44,7 +47,7 @@ class Decoder(
 
     private val codec = createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!)
     private val buffers by lazy { MediaCodecBuffers(codec) }
-    private var info = BufferInfo()
+    private var info = MediaCodec.BufferInfo()
     private val dropper = DecoderDropper(continuous)
 
     private var dequeuedInputs by observable(0) { _, _, _ -> printDequeued() }
@@ -95,7 +98,10 @@ class Decoder(
                 State.Wait
             }
             INFO_OUTPUT_FORMAT_CHANGED -> {
-                log.i("drain(): got INFO_OUTPUT_FORMAT_CHANGED, handling format and retrying. format=${codec.outputFormat}")
+                log.i(
+                    "drain(): got INFO_OUTPUT_FORMAT_CHANGED," +
+                        " handling format and retrying. format=${codec.outputFormat}"
+                )
                 next.handleRawFormat(codec.outputFormat)
                 State.Retry
             }
@@ -132,4 +138,4 @@ class Decoder(
         codec.stop()
         codec.release()
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
index 36711b36..e41db51e 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
@@ -1,3 +1,5 @@
+@file:Suppress("ReturnCount")
+
 package com.otaliastudios.transcoder.internal.codec
 
 import com.otaliastudios.transcoder.internal.utils.Logger
@@ -32,11 +34,11 @@ class DecoderDropper(private val continuous: Boolean) {
     private var firstOutputUs: Long? = null
 
     private fun debug(message: String, important: Boolean = false) {
-         val full = "$message firstInputUs=$firstInputUs " +
-                "validInputUs=[${closedRanges.joinToString {
-                    "$it(deltaUs=${closedDeltas[it]})"
-                }}] pendingRangeUs=${pendingRange}"
-        if (important) log.w(full) else log.v(full)
+        val full = "$message firstInputUs=$firstInputUs " +
+            "validInputUs=[${closedRanges.joinToString {
+                "$it(deltaUs=${closedDeltas[it]})"
+            }}] pendingRangeUs=$pendingRange"
+//        if (important) log.w(full) else log.v(full)
     }
 
     fun input(timeUs: Long, render: Boolean) {
@@ -89,4 +91,4 @@ class DecoderDropper(private val continuous: Boolean) {
         debug("OUTPUT: SKIPPING! outputTimeUs=$timeUs", important = true)
         return null
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderTimer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderTimer.kt
index ff3cb777..155b84b1 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderTimer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderTimer.kt
@@ -7,16 +7,16 @@ import com.otaliastudios.transcoder.time.TimeInterpolator
 import java.nio.ByteBuffer
 
 class DecoderTimerData(
-        buffer: ByteBuffer,
-        val rawTimeUs: Long,
-        timeUs: Long,
-        val timeStretch: Double,
-        release: (render: Boolean) -> Unit
+    buffer: ByteBuffer,
+    val rawTimeUs: Long,
+    timeUs: Long,
+    val timeStretch: Double,
+    release: (render: Boolean) -> Unit
 ) : DecoderData(buffer, timeUs, release)
 
 class DecoderTimer(
-        private val track: TrackType,
-        private val interpolator: TimeInterpolator,
+    private val track: TrackType,
+    private val interpolator: TimeInterpolator,
 ) : DataStep<DecoderData, DecoderChannel>() {
 
     private var lastTimeUs: Long? = null
@@ -41,12 +41,14 @@ class DecoderTimer(
         lastTimeUs = timeUs
         lastRawTimeUs = rawTimeUs
 
-        return State.Ok(DecoderTimerData(
+        return State.Ok(
+            DecoderTimerData(
                 buffer = state.value.buffer,
                 rawTimeUs = rawTimeUs,
                 timeUs = timeUs,
                 timeStretch = timeStretch,
                 release = state.value.release
-        ))
+            )
+        )
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt
index 8f4dfef4..6719ab3c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt
@@ -1,10 +1,15 @@
+@file:Suppress("MagicNumber")
+
 package com.otaliastudios.transcoder.internal.codec
 
 import android.media.MediaCodec
-import android.media.MediaCodec.*
+import android.media.MediaCodec.BUFFER_FLAG_CODEC_CONFIG
+import android.media.MediaCodec.BUFFER_FLAG_END_OF_STREAM
+import android.media.MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED
+import android.media.MediaCodec.INFO_OUTPUT_FORMAT_CHANGED
+import android.media.MediaCodec.INFO_TRY_AGAIN_LATER
 import android.view.Surface
 import com.otaliastudios.transcoder.common.TrackType
-import com.otaliastudios.transcoder.common.trackType
 import com.otaliastudios.transcoder.internal.Codecs
 import com.otaliastudios.transcoder.internal.data.WriterChannel
 import com.otaliastudios.transcoder.internal.data.WriterData
@@ -16,13 +21,12 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.internal.utils.trackMapOf
 import java.nio.ByteBuffer
 import java.util.concurrent.atomic.AtomicInteger
-import kotlin.properties.Delegates
 import kotlin.properties.Delegates.observable
 
 data class EncoderData(
-        val buffer: ByteBuffer?, // If present, it must have correct position/remaining!
-        val id: Int,
-        val timeUs: Long
+    val buffer: ByteBuffer?, // If present, it must have correct position/remaining!
+    val id: Int,
+    val timeUs: Long
 ) {
     companion object { val Empty = EncoderData(null, 0, 0L) }
 }
@@ -33,17 +37,17 @@ interface EncoderChannel : Channel {
 }
 
 class Encoder(
-        private val codec: MediaCodec,
-        override val surface: Surface?,
-        ownsCodecStart: Boolean,
-        private val ownsCodecStop: Boolean,
+    private val codec: MediaCodec,
+    override val surface: Surface?,
+    ownsCodecStart: Boolean,
+    private val ownsCodecStop: Boolean,
 ) : QueuedStep<EncoderData, EncoderChannel, WriterData, WriterChannel>(), EncoderChannel {
 
     constructor(codecs: Codecs, type: TrackType) : this(
-            codecs.encoders[type].first,
-            codecs.encoders[type].second,
-            codecs.ownsEncoderStart[type],
-            codecs.ownsEncoderStop[type]
+        codecs.encoders[type].first,
+        codecs.encoders[type].second,
+        codecs.ownsEncoderStart[type],
+        codecs.ownsEncoderStop[type]
     )
 
     companion object {
@@ -51,7 +55,7 @@ class Encoder(
     }
 
     private val type = if (surface != null) TrackType.VIDEO else TrackType.AUDIO
-    private val log = Logger("Encoder(${type},${ID[type].getAndIncrement()})")
+    private val log = Logger("Encoder($type,${ID[type].getAndIncrement()})")
     private var dequeuedInputs by observable(0) { _, _, _ -> printDequeued() }
     private var dequeuedOutputs by observable(0) { _, _, _ -> printDequeued() }
     private fun printDequeued() {
@@ -62,8 +66,7 @@ class Encoder(
 
     private val buffers by lazy { MediaCodecBuffers(codec) }
 
-    private var info = BufferInfo()
-
+    private var info = MediaCodec.BufferInfo()
 
     init {
         log.i("Encoder: ownsStart=$ownsCodecStart ownsStop=$ownsCodecStop")
@@ -159,9 +162,9 @@ class Encoder(
     }
 
     override fun release() {
-        log.i("release(): ownsStop=$ownsCodecStop dequeuedInputs=${dequeuedInputs} dequeuedOutputs=$dequeuedOutputs")
+        log.i("release(): ownsStop=$ownsCodecStop dequeuedInputs=$dequeuedInputs dequeuedOutputs=$dequeuedOutputs")
         if (ownsCodecStop) {
             codec.stop()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Bridge.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Bridge.kt
index e2c3b6e4..f5d23023 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Bridge.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Bridge.kt
@@ -8,8 +8,8 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
 
-class Bridge(private val format: MediaFormat)
-    : Step<ReaderData, ReaderChannel, WriterData, WriterChannel>, ReaderChannel {
+class Bridge(private val format: MediaFormat) :
+    Step<ReaderData, ReaderChannel, WriterData, WriterChannel>, ReaderChannel {
 
     private val log = Logger("Bridge")
     private val bufferSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
@@ -33,4 +33,4 @@ class Bridge(private val format: MediaFormat)
         val result = WriterData(chunk.buffer, chunk.timeUs, flags) {}
         return if (state is State.Eos) State.Eos(result) else State.Ok(result)
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt
index 7821cfb0..51ddf169 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt
@@ -8,7 +8,6 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.source.DataSource
 import java.nio.ByteBuffer
 
-
 data class ReaderData(val chunk: DataSource.Chunk, val id: Int)
 
 interface ReaderChannel : Channel {
@@ -16,8 +15,8 @@ interface ReaderChannel : Channel {
 }
 
 class Reader(
-        private val source: DataSource,
-        private val track: TrackType
+    private val source: DataSource,
+    private val track: TrackType
 ) : BaseStep<Unit, Channel, ReaderData, ReaderChannel>() {
 
     private val log = Logger("Reader")
@@ -55,4 +54,4 @@ class Reader(
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/ReaderTimer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/ReaderTimer.kt
index 8a7c5511..17ed6831 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/ReaderTimer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/ReaderTimer.kt
@@ -6,12 +6,12 @@ import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.time.TimeInterpolator
 
 class ReaderTimer(
-        private val track: TrackType,
-        private val interpolator: TimeInterpolator
+    private val track: TrackType,
+    private val interpolator: TimeInterpolator
 ) : DataStep<ReaderData, ReaderChannel>() {
     override fun step(state: State.Ok<ReaderData>, fresh: Boolean): State<ReaderData> {
         if (state is State.Eos) return state
         state.value.chunk.timeUs = interpolator.interpolate(track, state.value.chunk.timeUs)
         return state
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
index ba0ab702..70e27f03 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
@@ -8,30 +8,20 @@ import com.otaliastudios.transcoder.source.DataSource
 
 class Seeker(
     private val source: DataSource,
-    private val fetchPositions: () ->  List<Long>,
-    private val shouldFetch:() -> Boolean,
-    private val seek: (Long) -> Boolean
+    private val fetchPosition: () -> Long?,
+    private val shouldSeek: (Long) -> Boolean
 ) : BaseStep<Unit, Channel, Unit, Channel>() {
 
     private val log = Logger("Seeker")
     override val channel = Channel
 
-    private val positions:MutableList<Long> = mutableListOf()
-
     override fun step(state: State.Ok<Unit>, fresh: Boolean): State<Unit> {
-        if(positions.isEmpty() && shouldFetch()) {
-            positions.addAll(fetchPositions())
+        val position = fetchPosition()
+        if (position != null && shouldSeek(position)) {
+            log.i("Seeking to next position $position")
+            source.seekTo(position)
         }
 
-        if (positions.isNotEmpty()) {
-            if (seek(positions.first())) {
-                log.i("Seeking to next position ${positions.first()}")
-                val next = positions.removeFirst()
-                source.seekTo(next)
-            } else {
-                // log.v("Not seeking to next Request. head=${positions.first()}")
-            }
-        }
         return state
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
index 182d1679..ba875bef 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
@@ -11,10 +11,10 @@ import com.otaliastudios.transcoder.sink.DataSink
 import java.nio.ByteBuffer
 
 data class WriterData(
-        val buffer: ByteBuffer,
-        val timeUs: Long,
-        val flags: Int,
-        val release: () -> Unit
+    val buffer: ByteBuffer,
+    val timeUs: Long,
+    val flags: Int,
+    val release: () -> Unit
 )
 
 interface WriterChannel : Channel {
@@ -22,8 +22,8 @@ interface WriterChannel : Channel {
 }
 
 class Writer(
-        private val sink: DataSink,
-        private val track: TrackType
+    private val sink: DataSink,
+    private val track: TrackType
 ) : Step<WriterData, WriterChannel, Unit, Channel>, WriterChannel {
 
     override val channel = this
@@ -40,15 +40,15 @@ class Writer(
         val (buffer, timestamp, flags) = state.value
         val eos = state is State.Eos
         info.set(
-                buffer.position(),
-                buffer.remaining(),
-                timestamp,
-                if (eos) {
-                    flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM
-                } else flags
+            buffer.position(),
+            buffer.remaining(),
+            timestamp,
+            if (eos) {
+                flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM
+            } else flags
         )
         sink.writeTrack(track, buffer, info)
         state.value.release()
         return if (eos) State.Eos(Unit) else State.Ok(Unit)
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/CustomPipeline.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/CustomPipeline.kt
index 41938967..0fa4923d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/CustomPipeline.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/CustomPipeline.kt
@@ -25,7 +25,10 @@ class CustomPipeline private constructor(name: String, private val chain: List<A
             if (index < head) return@forEachIndexed
             val fresh = head == 0 || index != head
             state = executeStep(state, step, fresh) ?: run {
-                log.v("execute(): step ${step.name} (#$index/${chain.size}) is waiting. headState=$headState headIndex=$headIndex")
+                log.v(
+                    "execute(): step ${step.name} (#$index/${chain.size}) is waiting." +
+                        " headState=$headState headIndex=$headIndex"
+                )
                 return State.Wait
             }
             // log.v("execute(): executed ${step.name} (#$index/${chain.size}). result=$state")
@@ -56,8 +59,12 @@ class CustomPipeline private constructor(name: String, private val chain: List<A
 
     companion object {
         @Suppress("UNCHECKED_CAST")
-         fun build(name: String, builder: () -> Pipeline.Builder<*, EncoderChannel> = { Pipeline.Builder<EncoderData, EncoderChannel>() }): CustomPipeline {
+        fun build(
+            name: String,
+            builder: () -> Pipeline.Builder<*, EncoderChannel> =
+                { Pipeline.Builder<EncoderData, EncoderChannel>() }
+        ): CustomPipeline {
             return CustomPipeline(name, builder().steps as List<AnyStep>)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
index 39a41766..5a747e50 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
@@ -2,7 +2,6 @@ package com.otaliastudios.transcoder.internal.pipeline
 
 import com.otaliastudios.transcoder.internal.utils.Logger
 
-
 internal typealias AnyStep = Step<Any, Channel, Any, Channel>
 
 class Pipeline private constructor(name: String, private val chain: List<AnyStep>) {
@@ -26,7 +25,10 @@ class Pipeline private constructor(name: String, private val chain: List<AnyStep
             if (index < head) return@forEachIndexed
             val fresh = head == 0 || index != head
             state = executeStep(state, step, fresh) ?: run {
-                log.v("execute(): step ${step.name} (#$index/${chain.size}) is waiting. headState=$headState headIndex=$headIndex")
+                log.v(
+                    "execute(): step ${step.name} (#$index/${chain.size}) is waiting. " +
+                        "headState=$headState headIndex=$headIndex"
+                )
                 return State.Wait
             }
             // log.v("execute(): executed ${step.name} (#$index/${chain.size}). result=$state")
@@ -47,7 +49,11 @@ class Pipeline private constructor(name: String, private val chain: List<AnyStep
         chain.forEach { it.release() }
     }
 
-    private fun executeStep(previous: State.Ok<Any>, step: AnyStep, fresh: Boolean): State.Ok<Any>? {
+    private fun executeStep(
+        previous: State.Ok<Any>,
+        step: AnyStep,
+        fresh: Boolean
+    ): State.Ok<Any>? {
         return when (val state = step.step(previous, fresh)) {
             is State.Ok -> state
             is State.Retry -> executeStep(previous, step, fresh = false)
@@ -57,25 +63,25 @@ class Pipeline private constructor(name: String, private val chain: List<AnyStep
 
     companion object {
         @Suppress("UNCHECKED_CAST")
-         fun build(name: String, builder: () -> Builder<*, Channel> = { Builder<Unit, Channel>() }): Pipeline {
+        fun build(
+            name: String,
+            builder: () -> Builder<*, Channel> = { Builder<Unit, Channel>() }
+        ): Pipeline {
             return Pipeline(name, builder().steps as List<AnyStep>)
         }
     }
 
-    class Builder<D: Any, C: Channel> internal constructor(
-            internal val steps: List<Step<*, *, *, *>> = listOf()
+    class Builder<D : Any, C : Channel> internal constructor(
+        internal val steps: List<Step<*, *, *, *>> = listOf()
     ) {
-        operator fun <NewData: Any, NewChannel: Channel> plus(
-                step: Step<D, C, NewData, NewChannel>
-        ): Builder<NewData, NewChannel>  = Builder(steps + step)
+        operator fun <NewData : Any, NewChannel : Channel> plus(
+            step: Step<D, C, NewData, NewChannel>
+        ): Builder<NewData, NewChannel> = Builder(steps + step)
     }
 }
 
-operator fun <
-        CurrData: Any, CurrChannel: Channel,
-        NewData: Any, NewChannel: Channel
-> Step<Unit, Channel, CurrData, CurrChannel>.plus(
-        other: Step<CurrData, CurrChannel, NewData, NewChannel>
-): Pipeline.Builder<NewData, NewChannel> {
-    return Pipeline.Builder<CurrData, CurrChannel>(listOf(this)) + other
-}
\ No newline at end of file
+operator fun <CurrData : Any, CurrChannel : Channel, NewData : Any, NewChannel : Channel>
+Step<Unit, Channel, CurrData, CurrChannel>.plus(other: Step<CurrData, CurrChannel, NewData, NewChannel>):
+    Pipeline.Builder<NewData, NewChannel> {
+        return Pipeline.Builder<CurrData, CurrChannel>(listOf(this)) + other
+    }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/State.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/State.kt
index bdc81726..f33a99da 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/State.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/State.kt
@@ -21,4 +21,4 @@ sealed class State<out T> {
     object Retry : State<Nothing>() {
         override fun toString() = "State.Retry"
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt
index d0af8b33..4e61c993 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt
@@ -6,11 +6,11 @@ interface Channel {
 }
 
 interface Step<
-        Input: Any,
-        InputChannel: Channel,
-        Output: Any,
-        OutputChannel: Channel
-> {
+    Input : Any,
+    InputChannel : Channel,
+    Output : Any,
+    OutputChannel : Channel
+    > {
     val channel: InputChannel
 
     fun initialize(next: OutputChannel) = Unit
@@ -20,4 +20,4 @@ interface Step<
     fun release() = Unit
 }
 
-val Step<*, *, *, *>.name get() = this::class.simpleName
\ No newline at end of file
+val Step<*, *, *, *>.name get() = this::class.simpleName
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
index dab75de9..5e383640 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
@@ -1,3 +1,5 @@
+@file:Suppress("MagicNumber", "LongParameterList")
+
 package com.otaliastudios.transcoder.internal.pipeline
 
 import android.graphics.Point
@@ -20,86 +22,75 @@ import com.otaliastudios.transcoder.source.DataSource
 import com.otaliastudios.transcoder.stretch.AudioStretcher
 import com.otaliastudios.transcoder.time.TimeInterpolator
 
- fun EmptyPipeline() = Pipeline.build("Empty")
+fun emptyPipeline() = Pipeline.build("Empty")
 
-internal fun PassThroughPipeline(
-        track: TrackType,
-        source: DataSource,
-        sink: DataSink,
-        interpolator: TimeInterpolator
+internal fun passThroughPipeline(
+    track: TrackType,
+    source: DataSource,
+    sink: DataSink,
+    interpolator: TimeInterpolator
 ) = Pipeline.build("PassThrough($track)") {
     Reader(source, track) +
-            ReaderTimer(track, interpolator) +
-            Bridge(source.getTrackFormat(track)!!) +
-            Writer(sink, track)
+        ReaderTimer(track, interpolator) +
+        Bridge(source.getTrackFormat(track)!!) +
+        Writer(sink, track)
 }
 
-internal fun RegularPipeline(
-        track: TrackType,
-        source: DataSource,
-        sink: DataSink,
-        interpolator: TimeInterpolator,
-        format: MediaFormat,
-        codecs: Codecs,
-        videoRotation: Int,
-        audioStretcher: AudioStretcher,
-        audioResampler: AudioResampler
+internal fun regularPipeline(
+    track: TrackType,
+    source: DataSource,
+    sink: DataSink,
+    interpolator: TimeInterpolator,
+    format: MediaFormat,
+    codecs: Codecs,
+    videoRotation: Int,
+    audioStretcher: AudioStretcher,
+    audioResampler: AudioResampler
 ) = when (track) {
-    TrackType.VIDEO -> VideoPipeline(source, sink, interpolator, format, codecs, videoRotation)
-    TrackType.AUDIO -> AudioPipeline(source, sink, interpolator, format, codecs, audioStretcher, audioResampler)
+    TrackType.VIDEO -> videoPipeline(source, sink, interpolator, format, codecs, videoRotation)
+    TrackType.AUDIO -> audioPipeline(source, sink, interpolator, format, codecs, audioStretcher, audioResampler)
 }
 
 data class RenderingData(
-        val timeMsCurrent: Long,
-        val timeMsPrevious: Long,
-        val updateTime: () -> Unit
+    val timeMsCurrent: Long,
+    val timeMsPrevious: Long,
+    val updateTime: () -> Unit
 )
 
 interface RenderingChannel : Channel {
-        fun setDimensions(point: Point)
+    fun setDimensions(point: Point)
 }
-//lateinit var korgeDriver: Step<Unit, Channel, RenderingData, RenderingChannel>
-//lateinit var korgeRenderer: Step<RenderingData, RenderingChannel, Long, Channel>
-
-//fun setDriverStep(driver: Step<Unit, Channel, RenderingData, RenderingChannel>) {
-//        korgeDriver = driver
-//}
-//
-//fun setRendererStep(korgeR: Step<RenderingData, RenderingChannel, Long, Channel>){
-//        korgeRenderer = korgeR
-//}
 
-
-private fun VideoPipeline(
-        source: DataSource,
-        sink: DataSink,
-        interpolator: TimeInterpolator,
-        format: MediaFormat,
-        codecs: Codecs,
-        videoRotation: Int
+private fun videoPipeline(
+    source: DataSource,
+    sink: DataSink,
+    interpolator: TimeInterpolator,
+    format: MediaFormat,
+    codecs: Codecs,
+    videoRotation: Int
 ) = Pipeline.build("Video") {
     Reader(source, TrackType.VIDEO) +
-            Decoder(source.getTrackFormat(TrackType.VIDEO)!!, true) +
-            DecoderTimer(TrackType.VIDEO, interpolator) +
-            VideoRenderer(source.orientation, videoRotation, format) +
-            VideoPublisher() +
-            Encoder(codecs, TrackType.VIDEO) +
-            Writer(sink, TrackType.VIDEO)
+        Decoder(source.getTrackFormat(TrackType.VIDEO)!!, true) +
+        DecoderTimer(TrackType.VIDEO, interpolator) +
+        VideoRenderer(source.orientation, videoRotation, format) +
+        VideoPublisher() +
+        Encoder(codecs, TrackType.VIDEO) +
+        Writer(sink, TrackType.VIDEO)
 }
 
-private fun AudioPipeline(
-        source: DataSource,
-        sink: DataSink,
-        interpolator: TimeInterpolator,
-        format: MediaFormat,
-        codecs: Codecs,
-        audioStretcher: AudioStretcher,
-        audioResampler: AudioResampler
+private fun audioPipeline(
+    source: DataSource,
+    sink: DataSink,
+    interpolator: TimeInterpolator,
+    format: MediaFormat,
+    codecs: Codecs,
+    audioStretcher: AudioStretcher,
+    audioResampler: AudioResampler
 ) = Pipeline.build("Audio") {
     Reader(source, TrackType.AUDIO) +
-            Decoder(source.getTrackFormat(TrackType.AUDIO)!!, true) +
-            DecoderTimer(TrackType.AUDIO, interpolator) +
-            AudioEngine(audioStretcher, audioResampler, format) +
-            Encoder(codecs, TrackType.AUDIO) +
-            Writer(sink, TrackType.AUDIO)
-}
\ No newline at end of file
+        Decoder(source.getTrackFormat(TrackType.AUDIO)!!, true) +
+        DecoderTimer(TrackType.AUDIO, interpolator) +
+        AudioEngine(audioStretcher, audioResampler, format) +
+        Encoder(codecs, TrackType.AUDIO) +
+        Writer(sink, TrackType.AUDIO)
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/steps.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/steps.kt
index 42118528..37484d2f 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/steps.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/steps.kt
@@ -1,20 +1,20 @@
 package com.otaliastudios.transcoder.internal.pipeline
 
 abstract class BaseStep<
-        Input: Any,
-        InputChannel: Channel,
-        Output: Any,
-        OutputChannel: Channel
-> : Step<Input, InputChannel, Output, OutputChannel> {
+    Input : Any,
+    InputChannel : Channel,
+    Output : Any,
+    OutputChannel : Channel
+    > : Step<Input, InputChannel, Output, OutputChannel> {
     protected lateinit var next: OutputChannel
-    private set
+        private set
 
     override fun initialize(next: OutputChannel) {
         this.next = next
     }
 }
 
-abstract class DataStep<D: Any, C: Channel> : Step<D, C, D, C> {
+abstract class DataStep<D : Any, C : Channel> : Step<D, C, D, C> {
     override lateinit var channel: C
     override fun initialize(next: C) {
         channel = next
@@ -22,11 +22,11 @@ abstract class DataStep<D: Any, C: Channel> : Step<D, C, D, C> {
 }
 
 abstract class QueuedStep<
-        Input: Any,
-        InputChannel: Channel,
-        Output: Any,
-        OutputChannel: Channel
-> : BaseStep<Input, InputChannel, Output, OutputChannel>() {
+    Input : Any,
+    InputChannel : Channel,
+    Output : Any,
+    OutputChannel : Channel
+    > : BaseStep<Input, InputChannel, Output, OutputChannel>() {
 
     protected abstract fun enqueue(data: Input)
 
@@ -41,4 +41,4 @@ abstract class QueuedStep<
         }
         return drain()
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index f628c2a1..75969745 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -1,3 +1,5 @@
+@file:Suppress("MagicNumber")
+
 package com.otaliastudios.transcoder.internal.thumbnails
 
 import android.media.MediaFormat
@@ -29,11 +31,10 @@ import kotlinx.coroutines.isActive
 class DefaultThumbnailsEngine(
     private val dataSources: DataSources,
     private val rotation: Int,
-    resizer: Resizer,
-    requests: List<ThumbnailRequest>
+    resizer: Resizer
 ) : ThumbnailsEngine() {
 
-    private var fetch: Boolean = false
+    private var shouldSeek = true
     private var finish = false
     private val log = Logger("ThumbnailsEngine")
 
@@ -46,7 +47,10 @@ class DefaultThumbnailsEngine(
                 .addResizer(resizer)
                 .build(),
             audio = RemoveTrackStrategy()
-        ), dataSources, rotation, true
+        ),
+        dataSources,
+        rotation,
+        true
     )
 
     private val segments = Segments(dataSources, tracks, ::createPipeline, false)
@@ -63,9 +67,12 @@ class DefaultThumbnailsEngine(
         val localizedUs: Long
     ) {
         var actualLocalizedUs: Long = localizedUs
+        override fun toString(): String {
+            return positionUs.toString()
+        }
     }
 
-    private var stubs = mutableListOf<Stub>()
+    private val stubs = ArrayDeque<Stub>()
 
     private fun createPipeline(
         type: TrackType,
@@ -73,28 +80,32 @@ class DefaultThumbnailsEngine(
         status: TrackStatus,
         outputFormat: MediaFormat
     ): Pipeline {
-
         val source = dataSources[type][index]
-//            .forcingEos {
-//            stubs.isEmpty() && finish
-//        }
         return Pipeline.build("Thumbnails") {
-            Seeker(source, fetchPositions, shouldFetch) { it == stubs.firstOrNull()?.localizedUs } +
-                    Reader(source, type) +
-                    Decoder(source.getTrackFormat(type)!!, continuous = false) +
-                    VideoRenderer(source.orientation, rotation, outputFormat, flipY = true, true) {
-                        stubs.firstOrNull()?.positionUs ?: -1
-                    } +
-                    VideoSnapshots(outputFormat, fetchPositions, 500 * 1000) { pos, bitmap ->
-                        val stub = stubs.removeFirst()
-                        stub.actualLocalizedUs = pos
-                        log.i("Got snapshot. positionUs=${stub.positionUs} " +
-                                "localizedUs=${stub.localizedUs} " +
-                                "actualLocalizedUs=${stub.actualLocalizedUs} " +
-                                "deltaUs=${stub.localizedUs - stub.actualLocalizedUs}")
-                        val thumbnail = Thumbnail(stub.request, stub.positionUs, bitmap)
-                        progress(thumbnail)
-                    }
+            Seeker(source, fetchPosition) {
+                shouldSeek.also {
+                    shouldSeek = false
+                }
+                //                log.i("Seeker state check: $it :$stubs")
+            } +
+                Reader(source, type) +
+                Decoder(source.getTrackFormat(type)!!, continuous = false) +
+                VideoRenderer(source.orientation, rotation, outputFormat, flipY = true, true) {
+                    stubs.firstOrNull()?.positionUs ?: -1
+                } +
+                VideoSnapshots(outputFormat, fetchPosition, snapshotAccuracyUs) { pos, bitmap ->
+                    shouldSeek = true
+                    val stub = stubs.removeFirst()
+                    stub.actualLocalizedUs = pos
+                    log.i(
+                        "Got snapshot. positionUs=${stub.positionUs} " +
+                            "localizedUs=${stub.localizedUs} " +
+                            "actualLocalizedUs=${stub.actualLocalizedUs} " +
+                            "deltaUs=${stub.localizedUs - stub.actualLocalizedUs}"
+                    )
+                    val thumbnail = Thumbnail(stub.request, stub.positionUs, bitmap)
+                    progress(thumbnail)
+                }
         }
     }
 
@@ -110,6 +121,7 @@ class DefaultThumbnailsEngine(
             val advanced = segments.next(TrackType.VIDEO)?.advance() ?: false
             val completed = !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
             if (completed || stubs.isEmpty()) {
+                log.i("loop broken $stubs")
                 break
             } else if (!advanced) {
                 delay(WAIT_MS)
@@ -122,6 +134,17 @@ class DefaultThumbnailsEngine(
         segments.release()
     }
 
+    override suspend fun removePosition(positionUs: Long) {
+        if (positionUs == stubs.firstOrNull()?.positionUs) {
+            return
+        }
+        val stub = stubs.find { it.positionUs == positionUs }
+        if (stub != null) {
+            log.i("removePosition Match: $positionUs :$stubs")
+            stubs.remove(stub)
+        }
+    }
+
     private fun updatePositions(requests: List<ThumbnailRequest>, index: Int) {
         val positions = requests.flatMap { request ->
             val duration = timer.totalDurationUs
@@ -129,21 +152,15 @@ class DefaultThumbnailsEngine(
         }.sortedBy { it.first }
         log.i("Creating pipeline #$index. absoluteUs=${positions.joinToString { it.first.toString() }}")
 
-        stubs.addAll(positions.mapNotNull { (positionUs, request) ->
-            val localizedUs = timer.localize(TrackType.VIDEO, index, positionUs)
-            localizedUs?.let { Stub(request, positionUs, localizedUs) }
-        }.toMutableList())
-        fetch = true
+        stubs.addAll(
+            positions.mapNotNull { (positionUs, request) ->
+                val localizedUs = timer.localize(TrackType.VIDEO, index, positionUs)
+                localizedUs?.let { Stub(request, positionUs, localizedUs) }
+            }.toMutableList()
+        )
     }
 
-    private val shouldFetch:() -> Boolean = {
-        fetch.also {
-            fetch = false
-        }
-    }
-    private val fetchPositions: () -> List<Long> = {
-        stubs.map { it.localizedUs }
-    }
+    private val fetchPosition: () -> Long? = { stubs.firstOrNull()?.localizedUs }
 
     override fun cleanup() {
         runCatching { segments.release() }
@@ -151,7 +168,8 @@ class DefaultThumbnailsEngine(
     }
 
     companion object {
-        private val WAIT_MS = 10L
-        private val PROGRESS_LOOPS = 10L
+        private const val WAIT_MS = 10L
+        private const val PROGRESS_LOOPS = 10L
+        private const val snapshotAccuracyUs = 500 * 1000L
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index 96d0b02d..89aa8f47 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -1,3 +1,5 @@
+@file:Suppress("TooGenericExceptionCaught")
+
 package com.otaliastudios.transcoder.internal.thumbnails
 
 import com.otaliastudios.transcoder.ThumbnailerOptions
@@ -10,15 +12,19 @@ abstract class ThumbnailsEngine {
 
     abstract suspend fun queueThumbnails(list: List<ThumbnailRequest>, progress: (Thumbnail) -> Unit)
 
+    abstract suspend fun removePosition(positionUs: Long)
+
     abstract fun cleanup()
 
     companion object {
         private val log = Logger("ThumbnailsEngine")
 
         private fun Throwable.isInterrupted(): Boolean {
-            if (this is InterruptedException) return true
-            if (this == this.cause) return false
-            return this.cause?.isInterrupted() ?: false
+            return when {
+                this is InterruptedException -> true
+                this == this.cause -> false
+                else -> this.cause?.isInterrupted() ?: false
+            }
         }
 
         var engine: ThumbnailsEngine? = null
@@ -29,16 +35,15 @@ abstract class ThumbnailsEngine {
             log.i("thumbnails(): called...")
             dispatcher = ThumbnailsDispatcher(options)
 
-             engine = DefaultThumbnailsEngine(
+            engine = DefaultThumbnailsEngine(
                 dataSources = DataSources(options),
                 rotation = options.rotation,
-                resizer = options.resizer,
-                requests = options.thumbnailRequests
+                resizer = options.resizer
             )
             return engine
         }
     }
-    suspend fun queue(list: List<ThumbnailRequest>){
+    suspend fun queue(list: List<ThumbnailRequest>) {
         engine?.queueThumbnails(list) {
             dispatcher.dispatchThumbnail(it)
         }
@@ -57,7 +62,5 @@ abstract class ThumbnailsEngine {
         } finally {
             engine?.cleanup()
         }
-
-
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
index 201bd4b1..571965d1 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/DefaultTranscodeEngine.kt
@@ -1,3 +1,5 @@
+@file:Suppress("MagicNumber", "LongParameterList")
+
 package com.otaliastudios.transcoder.internal.transcode
 
 import android.media.MediaFormat
@@ -9,10 +11,10 @@ import com.otaliastudios.transcoder.internal.Segment
 import com.otaliastudios.transcoder.internal.Segments
 import com.otaliastudios.transcoder.internal.Timer
 import com.otaliastudios.transcoder.internal.Tracks
-import com.otaliastudios.transcoder.internal.pipeline.EmptyPipeline
-import com.otaliastudios.transcoder.internal.pipeline.PassThroughPipeline
 import com.otaliastudios.transcoder.internal.pipeline.Pipeline
-import com.otaliastudios.transcoder.internal.pipeline.RegularPipeline
+import com.otaliastudios.transcoder.internal.pipeline.emptyPipeline
+import com.otaliastudios.transcoder.internal.pipeline.passThroughPipeline
+import com.otaliastudios.transcoder.internal.pipeline.regularPipeline
 import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.internal.utils.TrackMap
 import com.otaliastudios.transcoder.internal.utils.forcingEos
@@ -34,7 +36,7 @@ class DefaultTranscodeEngine(
     private val audioResampler: AudioResampler,
     interpolator: TimeInterpolator,
     private val pipelineFactory: ((TrackType, DataSink, Codecs, MediaFormat) -> Pipeline)?,
-    ) : TranscodeEngine() {
+) : TranscodeEngine() {
 
     private val log = Logger("TranscodeEngine")
 
@@ -76,16 +78,19 @@ class DefaultTranscodeEngine(
             timer.positionUs[type] > timer.totalDurationUs + 100L
         }
         val sink = dataSink.ignoringEos { index < sources.lastIndex }
-        if (pipelineFactory != null)
-            return pipelineFactory.invoke(type, sink, codecs,outputFormat)
+        if (pipelineFactory != null) {
+            return pipelineFactory.invoke(type, sink, codecs, outputFormat)
+        }
 
         return when (status) {
-            TrackStatus.ABSENT -> EmptyPipeline()
-            TrackStatus.REMOVING -> EmptyPipeline()
-            TrackStatus.PASS_THROUGH -> PassThroughPipeline(type, source, sink, interpolator)
-            TrackStatus.COMPRESSING -> RegularPipeline(type,
-                    source, sink, interpolator, outputFormat, codecs,
-                    videoRotation, audioStretcher, audioResampler)
+            TrackStatus.ABSENT -> emptyPipeline()
+            TrackStatus.REMOVING -> emptyPipeline()
+            TrackStatus.PASS_THROUGH -> passThroughPipeline(type, source, sink, interpolator)
+            TrackStatus.COMPRESSING -> regularPipeline(
+                type,
+                source, sink, interpolator, outputFormat, codecs,
+                videoRotation, audioStretcher, audioResampler
+            )
         }
     }
 
@@ -104,7 +109,8 @@ class DefaultTranscodeEngine(
      */
     override fun transcode(progress: (Double) -> Unit) {
         var loop = 0L
-        log.i("transcode(): about to start, " +
+        log.i(
+            "transcode(): about to start, " +
                 "durationUs=${timer.totalDurationUs}, " +
                 "audioUs=${timer.durationUs.audioOrNull()}, " +
                 "videoUs=${timer.durationUs.videoOrNull()}"
@@ -143,9 +149,8 @@ class DefaultTranscodeEngine(
         runCatching { codecs.release() }
     }
 
-
     companion object {
-        private val WAIT_MS = 10L
-        private val PROGRESS_LOOPS = 10L
+        private const val WAIT_MS = 10L
+        private const val PROGRESS_LOOPS = 10L
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
index 940b4469..6c33950d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt
@@ -1,3 +1,5 @@
+@file:Suppress("TooGenericExceptionCaught", "UnnecessaryAbstractClass")
+
 package com.otaliastudios.transcoder.internal.transcode
 
 import android.media.MediaFormat
@@ -23,33 +25,35 @@ abstract class TranscodeEngine {
         private val log = Logger("TranscodeEngine")
 
         private fun Throwable.isInterrupted(): Boolean {
-            if (this is InterruptedException) return true
-            if (this == this.cause) return false
-            return this.cause?.isInterrupted() ?: false
+            return when {
+                this is InterruptedException -> true
+                this == this.cause -> false
+                else -> this.cause?.isInterrupted() ?: false
+            }
         }
 
         @JvmStatic
         fun transcode(
             options: TranscoderOptions,
             function: ((TrackType, DataSink, Codecs, MediaFormat) -> Pipeline)?,
-            ) {
+        ) {
             log.i("transcode(): called...")
             var engine: TranscodeEngine? = null
             val dispatcher = TranscodeDispatcher(options)
             try {
                 engine = DefaultTranscodeEngine(
-                        dataSources = DataSources(options),
-                        dataSink = options.dataSink,
-                        strategies = trackMapOf(
-                                video = options.videoTrackStrategy,
-                                audio = options.audioTrackStrategy
-                        ),
-                        validator = options.validator,
-                        videoRotation = options.videoRotation,
-                        interpolator = options.timeInterpolator,
-                        audioStretcher = options.audioStretcher,
-                        audioResampler = options.audioResampler,
-                        pipelineFactory = function
+                    dataSources = DataSources(options),
+                    dataSink = options.dataSink,
+                    strategies = trackMapOf(
+                        video = options.videoTrackStrategy,
+                        audio = options.audioTrackStrategy
+                    ),
+                    validator = options.validator,
+                    videoRotation = options.videoRotation,
+                    interpolator = options.timeInterpolator,
+                    audioStretcher = options.audioStretcher,
+                    audioResampler = options.audioResampler,
+                    pipelineFactory = function
                 )
                 if (!engine.validate()) {
                     dispatcher.dispatchSuccess(Transcoder.SUCCESS_NOT_NEEDED)
@@ -73,4 +77,4 @@ abstract class TranscodeEngine {
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/ThreadPool.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/ThreadPool.kt
index 879920d6..2263d762 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/ThreadPool.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/ThreadPool.kt
@@ -1,3 +1,5 @@
+@file:Suppress("MagicNumber")
+
 package com.otaliastudios.transcoder.internal.utils
 
 import java.util.concurrent.LinkedBlockingQueue
@@ -15,15 +17,16 @@ internal object ThreadPool {
      */
     @JvmStatic
     val executor = ThreadPoolExecutor(
-            Runtime.getRuntime().availableProcessors() + 1,
-            Runtime.getRuntime().availableProcessors() + 1,
-            60,
-            TimeUnit.SECONDS,
-            LinkedBlockingQueue(),
-            object : ThreadFactory {
-                private val count = AtomicInteger(1)
-                override fun newThread(r: Runnable): Thread {
-                    return Thread(r, "TranscoderThread #" + count.getAndIncrement())
-                }
-            })
-}
\ No newline at end of file
+        Runtime.getRuntime().availableProcessors() + 1,
+        Runtime.getRuntime().availableProcessors() + 1,
+        60,
+        TimeUnit.SECONDS,
+        LinkedBlockingQueue(),
+        object : ThreadFactory {
+            private val count = AtomicInteger(1)
+            override fun newThread(r: Runnable): Thread {
+                return Thread(r, "TranscoderThread #" + count.getAndIncrement())
+            }
+        }
+    )
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/TrackMap.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/TrackMap.kt
index 46e64309..38877a63 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/TrackMap.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/TrackMap.kt
@@ -54,4 +54,3 @@ private class DefaultTrackMap<T>(video: T?, audio: T?) : MutableTrackMap<T> {
         map[type] = value
     }
 }
-
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/debug.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/debug.kt
index d8dd1a47..ad01eb8e 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/debug.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/debug.kt
@@ -1,3 +1,5 @@
+@file:Suppress("MagicNumber")
+
 package com.otaliastudios.transcoder.internal.utils
 
-internal fun stackTrace() = Thread.currentThread().stackTrace.drop(2).take(10).joinToString("\n")
\ No newline at end of file
+internal fun stackTrace() = Thread.currentThread().stackTrace.drop(2).take(10).joinToString("\n")
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt
index c093facf..ee8fa271 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt
@@ -2,19 +2,17 @@ package com.otaliastudios.transcoder.internal.utils
 
 import android.media.MediaCodec
 import com.otaliastudios.transcoder.common.TrackType
-import com.otaliastudios.transcoder.internal.Segment
-import com.otaliastudios.transcoder.internal.Segments
 import com.otaliastudios.transcoder.sink.DataSink
 import com.otaliastudios.transcoder.source.DataSource
 import java.nio.ByteBuffer
 
 // See https://github.com/natario1/Transcoder/issues/107
-internal fun DataSink.ignoringEos(ignore: () -> Boolean): DataSink
-        = EosIgnoringDataSink(this, ignore)
+internal fun DataSink.ignoringEos(ignore: () -> Boolean): DataSink =
+    EosIgnoringDataSink(this, ignore)
 
 private class EosIgnoringDataSink(
-        private val sink: DataSink,
-        private val ignore: () -> Boolean,
+    private val sink: DataSink,
+    private val ignore: () -> Boolean,
 ) : DataSink by sink {
     private val info = MediaCodec.BufferInfo()
     override fun writeTrack(type: TrackType, byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
@@ -35,14 +33,14 @@ private class EosIgnoringDataSink(
  * This can happen if the user adds e.g. 1 minute of audio with 20 seconds of video.
  * In this case the video track must be stopped once the audio stops.
  */
-internal fun DataSource.forcingEos(force: () -> Boolean): DataSource
-        = EosForcingDataSource(this, force)
+internal fun DataSource.forcingEos(force: () -> Boolean): DataSource =
+    EosForcingDataSource(this, force)
 
 private class EosForcingDataSource(
-        private val source: DataSource,
-        private val force: () -> Boolean,
+    private val source: DataSource,
+    private val force: () -> Boolean,
 ) : DataSource by source {
     override fun isDrained(): Boolean {
         return force() || source.isDrained
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt
index f8ca5f86..5c0043f3 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt
@@ -1,3 +1,5 @@
+@file:Suppress("MagicNumber")
+
 package com.otaliastudios.transcoder.internal.video
 
 import com.otaliastudios.transcoder.internal.utils.Logger
@@ -10,7 +12,7 @@ interface FrameDropper {
  * A very simple dropper, from
  * https://stackoverflow.com/questions/4223766/dropping-video-frames
  */
-fun FrameDropper(inputFps: Int, outputFps: Int) = object : FrameDropper {
+fun frameDropper(inputFps: Int, outputFps: Int) = object : FrameDropper {
 
     private val log = Logger("FrameDropper")
     private val inputSpf = 1.0 / inputFps
@@ -23,16 +25,20 @@ fun FrameDropper(inputFps: Int, outputFps: Int) = object : FrameDropper {
         val timeS = timeUs / 1000.0 / 1000.0
         currentSpf += timeS - previousTs
         previousTs = timeS
-        if (frameCount++ == 0) {
-            log.v("RENDERING (first frame) - currentSpf=$currentSpf inputSpf=$inputSpf outputSpf=$outputSpf")
-            return true
-        } else if (currentSpf > outputSpf) {
-            currentSpf -= outputSpf
-            log.v("RENDERING - currentSpf=$currentSpf inputSpf=$inputSpf outputSpf=$outputSpf")
-            return true
-        } else {
-            log.v("DROPPING - currentSpf=$currentSpf inputSpf=$inputSpf outputSpf=$outputSpf")
-            return false
+        return when {
+            frameCount++ == 0 -> {
+                log.v("RENDERING (first frame) - currentSpf=$currentSpf inputSpf=$inputSpf outputSpf=$outputSpf")
+                true
+            }
+            currentSpf > outputSpf -> {
+                currentSpf -= outputSpf
+                log.v("RENDERING - currentSpf=$currentSpf inputSpf=$inputSpf outputSpf=$outputSpf")
+                true
+            }
+            else -> {
+                log.v("DROPPING - currentSpf=$currentSpf inputSpf=$inputSpf outputSpf=$outputSpf")
+                false
+            }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoPublisher.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoPublisher.kt
index da1857be..cb69ccd3 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoPublisher.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoPublisher.kt
@@ -1,3 +1,5 @@
+@file:Suppress("MagicNumber")
+
 package com.otaliastudios.transcoder.internal.video
 
 import android.opengl.EGL14
@@ -9,8 +11,7 @@ import com.otaliastudios.transcoder.internal.pipeline.Channel
 import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.internal.pipeline.Step
 
-
-class VideoPublisher: Step<Long, Channel, EncoderData, EncoderChannel> {
+class VideoPublisher : Step<Long, Channel, EncoderData, EncoderChannel> {
 
     override val channel = Channel
 
@@ -24,12 +25,12 @@ class VideoPublisher: Step<Long, Channel, EncoderData, EncoderChannel> {
     }
 
     override fun step(state: State.Ok<Long>, fresh: Boolean): State<EncoderData> {
-        if (state is State.Eos) {
-            return State.Eos(EncoderData.Empty)
+        return if (state is State.Eos) {
+            State.Eos(EncoderData.Empty)
         } else {
-            surface.setPresentationTime(state.value * 1000)
+            surface.setPresentationTime(state.value * 1000L)
             surface.swapBuffers()
-            return State.Ok(EncoderData.Empty)
+            State.Ok(EncoderData.Empty)
         }
     }
 
@@ -37,4 +38,4 @@ class VideoPublisher: Step<Long, Channel, EncoderData, EncoderChannel> {
         surface.release()
         core.release()
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
index 25910b44..06dbaf2c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
@@ -1,8 +1,11 @@
+@file:Suppress("MagicNumber")
+
 package com.otaliastudios.transcoder.internal.video
 
-import android.graphics.Bitmap
 import android.media.MediaFormat
-import android.media.MediaFormat.*
+import android.media.MediaFormat.KEY_FRAME_RATE
+import android.media.MediaFormat.KEY_HEIGHT
+import android.media.MediaFormat.KEY_WIDTH
 import android.view.Surface
 import com.otaliastudios.transcoder.internal.codec.DecoderChannel
 import com.otaliastudios.transcoder.internal.codec.DecoderData
@@ -12,16 +15,15 @@ import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.internal.pipeline.Step
 import com.otaliastudios.transcoder.internal.utils.Logger
 
-
 class VideoRenderer(
-        private val sourceRotation: Int, // intrinsic source rotation
-        private val extraRotation: Int, // any extra rotation in TranscoderOptions
-        private val targetFormat: MediaFormat,
-        flipY: Boolean = false,
-        private val isThumbnailer: Boolean = false,
-        private val getTargetTs: (() -> Long)? = null
+    private val sourceRotation: Int, // intrinsic source rotation
+    private val extraRotation: Int, // any extra rotation in TranscoderOptions
+    private val targetFormat: MediaFormat,
+    flipY: Boolean = false,
+    private val isThumbnailer: Boolean = false,
+    private val getTargetTs: (() -> Long)? = null
 
-): Step<DecoderData, DecoderChannel, Long, Channel>, DecoderChannel {
+) : Step<DecoderData, DecoderChannel, Long, Channel>, DecoderChannel {
 
     private val log = Logger("VideoRenderer")
 
@@ -74,8 +76,16 @@ class VideoRenderer(
         val sourceWidth = sourceFormat.getInteger(KEY_WIDTH).toFloat()
         val sourceHeight = sourceFormat.getInteger(KEY_HEIGHT).toFloat()
         val sourceRatio = sourceWidth / sourceHeight
-        val targetWidth = (if (flip) targetFormat.getInteger(KEY_HEIGHT) else targetFormat.getInteger(KEY_WIDTH)).toFloat()
-        val targetHeight = (if (flip) targetFormat.getInteger(KEY_WIDTH) else targetFormat.getInteger(KEY_HEIGHT)).toFloat()
+        val targetWidth = (
+            if (flip) targetFormat.getInteger(KEY_HEIGHT) else {
+                targetFormat.getInteger(KEY_WIDTH)
+            }
+            ).toFloat()
+        val targetHeight = (
+            if (flip) targetFormat.getInteger(KEY_WIDTH) else {
+                targetFormat.getInteger(KEY_HEIGHT)
+            }
+            ).toFloat()
         val targetRatio = targetWidth / targetHeight
         var scaleX = 1f
         var scaleY = 1f
@@ -87,9 +97,7 @@ class VideoRenderer(
         frameDrawer.setScale(scaleX, scaleY)
 
         // Create the frame dropper, now that we know the source FPS and the target FPS.
-        frameDropper = FrameDropper(
-                sourceFormat.getInteger(KEY_FRAME_RATE),
-                targetFormat.getInteger(KEY_FRAME_RATE))
+        frameDropper = frameDropper(sourceFormat.getInteger(KEY_FRAME_RATE), targetFormat.getInteger(KEY_FRAME_RATE))
         return frameDrawer.surface
     }
 
@@ -100,8 +108,9 @@ class VideoRenderer(
             state.value.release(false)
             State.Eos(0L)
         } else {
-            if ((isThumbnailer && shouldRenderThumbnail(state.value.timeUs, getTargetTs?.invoke() ?: -1))
-                || frameDropper.shouldRender(state.value.timeUs)) {
+            if ((isThumbnailer && shouldRenderThumbnail(state.value.timeUs, getTargetTs?.invoke() ?: -1)) ||
+                frameDropper.shouldRender(state.value.timeUs)
+            ) {
                 state.value.release(true)
                 frameDrawer.drawFrame()
                 State.Ok(state.value.timeUs)
@@ -124,4 +133,4 @@ class VideoRenderer(
     override fun release() {
         frameDrawer.release()
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt
index 2793331e..a6f5cca0 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt
@@ -1,3 +1,5 @@
+@file:Suppress("MagicNumber")
+
 package com.otaliastudios.transcoder.internal.video
 
 import android.graphics.Bitmap
@@ -9,26 +11,23 @@ import android.opengl.GLES20
 import com.otaliastudios.opengl.core.EglCore
 import com.otaliastudios.opengl.core.Egloo
 import com.otaliastudios.opengl.surface.EglOffscreenSurface
-import com.otaliastudios.opengl.surface.EglSurface
 import com.otaliastudios.transcoder.internal.pipeline.BaseStep
 import com.otaliastudios.transcoder.internal.pipeline.Channel
 import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.internal.utils.Logger
-import com.otaliastudios.transcoder.thumbnail.Thumbnail
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
 import kotlin.math.abs
 
 class VideoSnapshots(
-        format: MediaFormat,
-        private val fetchRequests:()-> List<Long>,
-        private val accuracyUs: Long,
-        private val onSnapshot: (Long, Bitmap) -> Unit
+    format: MediaFormat,
+    private val fetchRequest: () -> Long?,
+    private val accuracyUs: Long,
+    private val onSnapshot: (Long, Bitmap) -> Unit
 ) : BaseStep<Long, Channel, Long, Channel>() {
 
     private val log = Logger("VideoSnapshots")
     override val channel = Channel
-    private val requests = mutableListOf<Long>()
     private val width = format.getInteger(KEY_WIDTH)
     private val height = format.getInteger(KEY_HEIGHT)
     private val core = EglCore(EGL14.EGL_NO_CONTEXT, EglCore.FLAG_RECORDABLE)
@@ -37,18 +36,11 @@ class VideoSnapshots(
     }
 
     override fun step(state: State.Ok<Long>, fresh: Boolean): State<Long> {
-        if (requests.isEmpty()){
-            requests.addAll(fetchRequests())
-            if (requests.isEmpty()) {
-                return state
-            }
-        }
+        val expectedUs = fetchRequest() ?: return state
 
-        val expectedUs = requests.first()
         val deltaUs = abs(expectedUs - state.value)
         if (deltaUs < accuracyUs || (state is State.Eos && expectedUs > state.value)) {
             log.i("Request MATCHED! expectedUs=$expectedUs actualUs=${state.value} deltaUs=$deltaUs")
-            requests.removeFirst()
             val buffer = ByteBuffer.allocateDirect(width * height * 4)
             buffer.order(ByteOrder.LITTLE_ENDIAN)
             GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer)
@@ -67,4 +59,4 @@ class VideoSnapshots(
         surface.release()
         core.release()
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/CoverThumbnailRequest.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/CoverThumbnailRequest.kt
index 4edebede..a511e5be 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/CoverThumbnailRequest.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/CoverThumbnailRequest.kt
@@ -2,4 +2,4 @@ package com.otaliastudios.transcoder.thumbnail
 
 class CoverThumbnailRequest : ThumbnailRequest {
     override fun locate(durationUs: Long) = listOf(0L)
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
index 5962ca09..d852f005 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
@@ -7,4 +7,4 @@ open class SingleThumbnailRequest(private val positionUs: Long) : ThumbnailReque
         }
         return listOf(positionUs)
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/Thumbnail.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/Thumbnail.kt
index 760ad983..dcebe09c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/Thumbnail.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/Thumbnail.kt
@@ -3,7 +3,7 @@ package com.otaliastudios.transcoder.thumbnail
 import android.graphics.Bitmap
 
 class Thumbnail internal constructor(
-        val request: ThumbnailRequest,
-        val positionUs: Long,
-        val bitmap: Bitmap
-)
\ No newline at end of file
+    val request: ThumbnailRequest,
+    val positionUs: Long,
+    val bitmap: Bitmap
+)
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt
index 48019689..e2f19aed 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt
@@ -10,4 +10,4 @@ interface ThumbnailRequest {
 
     // Could add resizing per request
     // val resizer = PassThroughResizer()
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/UniformThumbnailRequest.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/UniformThumbnailRequest.kt
index b87bf385..ba250a78 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/UniformThumbnailRequest.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/UniformThumbnailRequest.kt
@@ -18,4 +18,4 @@ class UniformThumbnailRequest(private val count: Int) : ThumbnailRequest {
         }
         return list
     }
-}
\ No newline at end of file
+}

From a4393cc49635264697f473f0c550ecee3f4a3625 Mon Sep 17 00:00:00 2001
From: Sahil Bajaj <sahilbajaj.nc@gmail.com>
Date: Sun, 18 Jul 2021 12:28:24 +0530
Subject: [PATCH 028/101] Renderer refactor

---
 .../otaliastudios/transcoder/internal/pipeline/pipelines.kt   | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
index 5e383640..5d78e365 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
@@ -57,10 +57,6 @@ data class RenderingData(
     val updateTime: () -> Unit
 )
 
-interface RenderingChannel : Channel {
-    fun setDimensions(point: Point)
-}
-
 private fun videoPipeline(
     source: DataSource,
     sink: DataSink,

From bcaaf84ee4651374e43fa634f607fb0825a9f0e9 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Sun, 18 Jul 2021 17:54:58 +0530
Subject: [PATCH 029/101] code formatting

---
 .../main/java/com/otaliastudios/transcoder/internal/Codecs.kt | 2 ++
 .../main/java/com/otaliastudios/transcoder/internal/Timer.kt  | 2 +-
 .../otaliastudios/transcoder/internal/codec/DecoderDropper.kt | 2 +-
 .../otaliastudios/transcoder/internal/pipeline/Pipeline.kt    | 4 ++--
 .../otaliastudios/transcoder/internal/pipeline/pipelines.kt   | 1 -
 .../transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt | 2 +-
 6 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
index d0a01273..945f2e4b 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
@@ -1,3 +1,5 @@
+@file:Suppress("UnusedPrivateMember")
+
 package com.otaliastudios.transcoder.internal
 
 import android.media.MediaCodec
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt
index 307f3c5b..ce377219 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt
@@ -1,4 +1,4 @@
-@file:Suppress("MagicNumber")
+@file:Suppress("MagicNumber", "UnusedPrivateMember")
 
 package com.otaliastudios.transcoder.internal
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
index e41db51e..1b238b10 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
@@ -38,7 +38,7 @@ class DecoderDropper(private val continuous: Boolean) {
             "validInputUs=[${closedRanges.joinToString {
                 "$it(deltaUs=${closedDeltas[it]})"
             }}] pendingRangeUs=$pendingRange"
-//        if (important) log.w(full) else log.v(full)
+        if (important) log.w(full) else log.v(full)
     }
 
     fun input(timeUs: Long, render: Boolean) {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
index 5a747e50..f57bfc83 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Pipeline.kt
@@ -83,5 +83,5 @@ class Pipeline private constructor(name: String, private val chain: List<AnyStep
 operator fun <CurrData : Any, CurrChannel : Channel, NewData : Any, NewChannel : Channel>
 Step<Unit, Channel, CurrData, CurrChannel>.plus(other: Step<CurrData, CurrChannel, NewData, NewChannel>):
     Pipeline.Builder<NewData, NewChannel> {
-        return Pipeline.Builder<CurrData, CurrChannel>(listOf(this)) + other
-    }
+    return Pipeline.Builder<CurrData, CurrChannel>(listOf(this)) + other
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
index 5d78e365..c4845aed 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt
@@ -2,7 +2,6 @@
 
 package com.otaliastudios.transcoder.internal.pipeline
 
-import android.graphics.Point
 import android.media.MediaFormat
 import com.otaliastudios.transcoder.common.TrackType
 import com.otaliastudios.transcoder.internal.Codecs
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 75969745..4d90eff2 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -1,4 +1,4 @@
-@file:Suppress("MagicNumber")
+@file:Suppress("MagicNumber", "UnusedPrivateMember")
 
 package com.otaliastudios.transcoder.internal.thumbnails
 

From 24150e76807f86f25cce3c0fd6b08db42d407e46 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Mon, 19 Jul 2021 17:16:04 +0530
Subject: [PATCH 030/101] should flush condition for decoder

---
 .../otaliastudios/transcoder/internal/codec/Decoder.kt |  3 +++
 .../internal/thumbnails/DefaultThumbnailsEngine.kt     | 10 ++++++++--
 2 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index ed4ca3e9..69f189fc 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -36,6 +36,7 @@ interface DecoderChannel : Channel {
 class Decoder(
     private val format: MediaFormat, // source.getTrackFormat(track)
     continuous: Boolean, // relevant if the source sends no-render chunks. should we compensate or not?
+    val shouldFlush: (() -> Boolean)? = null
 ) : QueuedStep<ReaderData, ReaderChannel, DecoderData, DecoderChannel>(), ReaderChannel {
 
     companion object {
@@ -65,6 +66,8 @@ class Decoder(
     }
 
     override fun buffer(): Pair<ByteBuffer, Int>? {
+        if (shouldFlush?.invoke() == true)
+            codec.flush()
         val id = codec.dequeueInputBuffer(0)
         return if (id >= 0) {
             dequeuedInputs++
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 4d90eff2..0d7378aa 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -35,6 +35,7 @@ class DefaultThumbnailsEngine(
 ) : ThumbnailsEngine() {
 
     private var shouldSeek = true
+    private var shouldFlush = false
     private var finish = false
     private val log = Logger("ThumbnailsEngine")
 
@@ -89,12 +90,17 @@ class DefaultThumbnailsEngine(
                 //                log.i("Seeker state check: $it :$stubs")
             } +
                 Reader(source, type) +
-                Decoder(source.getTrackFormat(type)!!, continuous = false) +
+                Decoder(source.getTrackFormat(type)!!, continuous = false) {
+                    shouldFlush.also {
+                        shouldFlush = false
+                    }
+                } +
                 VideoRenderer(source.orientation, rotation, outputFormat, flipY = true, true) {
                     stubs.firstOrNull()?.positionUs ?: -1
                 } +
                 VideoSnapshots(outputFormat, fetchPosition, snapshotAccuracyUs) { pos, bitmap ->
                     shouldSeek = true
+                    shouldFlush = true
                     val stub = stubs.removeFirst()
                     stub.actualLocalizedUs = pos
                     log.i(
@@ -149,7 +155,7 @@ class DefaultThumbnailsEngine(
         val positions = requests.flatMap { request ->
             val duration = timer.totalDurationUs
             request.locate(duration).map { it to request }
-        }.sortedBy { it.first }
+        }
         log.i("Creating pipeline #$index. absoluteUs=${positions.joinToString { it.first.toString() }}")
 
         stubs.addAll(

From 2d899142007be4402c913aabcff897fa8cc3cd75 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Tue, 20 Jul 2021 00:08:43 +0530
Subject: [PATCH 031/101] selective seek conditions

---
 .../com/otaliastudios/transcoder/internal/data/Seeker.kt     | 5 +++--
 .../internal/thumbnails/DefaultThumbnailsEngine.kt           | 5 ++++-
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
index 70e27f03..a22cc953 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
@@ -18,10 +18,11 @@ class Seeker(
     override fun step(state: State.Ok<Unit>, fresh: Boolean): State<Unit> {
         val position = fetchPosition()
         if (position != null && shouldSeek(position)) {
-            log.i("Seeking to next position $position")
+            log.i("Seeking to next position $position where currentReaderTime=${source.getPositionUs()}")
             source.seekTo(position)
+        } else {
+            log.i("Skipping seek for $position where currentReaderTime=${source.getPositionUs()}")
         }
-
         return state
     }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 0d7378aa..071027f5 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -27,6 +27,7 @@ import com.otaliastudios.transcoder.time.DefaultTimeInterpolator
 import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.isActive
+import kotlin.math.ceil
 
 class DefaultThumbnailsEngine(
     private val dataSources: DataSources,
@@ -84,7 +85,9 @@ class DefaultThumbnailsEngine(
         val source = dataSources[type][index]
         return Pipeline.build("Thumbnails") {
             Seeker(source, fetchPosition) {
-                shouldSeek.also {
+                val nextSeek = ceil(source.getPositionUs().toDouble() / 1000000) * 1000000
+                val position = stubs.firstOrNull()?.positionUs ?: -1
+                !(position > source.getPositionUs() && position < nextSeek) && shouldSeek.also {
                     shouldSeek = false
                 }
                 //                log.i("Seeker state check: $it :$stubs")

From 1bb996481c8089153ec1bfa8b78b97dbaec34eb6 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Tue, 20 Jul 2021 12:28:05 +0530
Subject: [PATCH 032/101] flush logic

---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 071027f5..fb64bb00 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -39,7 +39,7 @@ class DefaultThumbnailsEngine(
     private var shouldFlush = false
     private var finish = false
     private val log = Logger("ThumbnailsEngine")
-
+    private var previousSnapshotUs = 0L
     // Huge framerate triks the VideoRenderer into not dropping frames, which is important
     // for thumbnail requests that want to catch the very last frame.
     private val tracks = Tracks(
@@ -85,11 +85,17 @@ class DefaultThumbnailsEngine(
         val source = dataSources[type][index]
         return Pipeline.build("Thumbnails") {
             Seeker(source, fetchPosition) {
-                val nextSeek = ceil(source.getPositionUs().toDouble() / 1000000) * 1000000
+                val nextSeek = (ceil(source.getPositionUs().toDouble() / 1000000) * 1000000L).toLong()
                 val position = stubs.firstOrNull()?.positionUs ?: -1
-                !(position > source.getPositionUs() && position < nextSeek) && shouldSeek.also {
+
+                val seek = !(position > previousSnapshotUs && position < nextSeek) && shouldSeek
+
+                if (seek)
+                {
+                    shouldFlush = true
                     shouldSeek = false
                 }
+                seek
                 //                log.i("Seeker state check: $it :$stubs")
             } +
                 Reader(source, type) +
@@ -103,9 +109,9 @@ class DefaultThumbnailsEngine(
                 } +
                 VideoSnapshots(outputFormat, fetchPosition, snapshotAccuracyUs) { pos, bitmap ->
                     shouldSeek = true
-                    shouldFlush = true
                     val stub = stubs.removeFirst()
                     stub.actualLocalizedUs = pos
+                    previousSnapshotUs = pos
                     log.i(
                         "Got snapshot. positionUs=${stub.positionUs} " +
                             "localizedUs=${stub.localizedUs} " +

From c8584b8f9d0f536c47eedb1ba89ab2d3c72b4218 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Tue, 20 Jul 2021 17:13:08 +0530
Subject: [PATCH 033/101] code formatting

---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt          | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index fb64bb00..b4412ccf 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -85,13 +85,12 @@ class DefaultThumbnailsEngine(
         val source = dataSources[type][index]
         return Pipeline.build("Thumbnails") {
             Seeker(source, fetchPosition) {
-                val nextSeek = (ceil(source.getPositionUs().toDouble() / 1000000) * 1000000L).toLong()
+                val nextSeek = (ceil(source.getPositionUs().toDouble() / KEY_FRAME) * KEY_FRAME).toLong()
                 val position = stubs.firstOrNull()?.positionUs ?: -1
 
                 val seek = !(position > previousSnapshotUs && position < nextSeek) && shouldSeek
 
-                if (seek)
-                {
+                if (seek) {
                     shouldFlush = true
                     shouldSeek = false
                 }
@@ -186,5 +185,6 @@ class DefaultThumbnailsEngine(
         private const val WAIT_MS = 10L
         private const val PROGRESS_LOOPS = 10L
         private const val snapshotAccuracyUs = 500 * 1000L
+        private const val KEY_FRAME = 1000 * 1000L
     }
 }

From c8882c7ee1d5bd7addee0521a72d0a7aba20212e Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Wed, 15 Sep 2021 13:22:47 +0530
Subject: [PATCH 034/101] wave file reader and header updater along with a raw
 audio data source

---
 .../transcoder/common/WavFile.java            | 302 ++++++++++++++++++
 .../transcoder/internal/utils/eos.kt          |  11 +
 .../transcoder/sink/RawDataSink.kt            |  35 ++
 .../transcoder/source/RawAudioDataSource.kt   | 102 ++++++
 4 files changed, 450 insertions(+)
 create mode 100644 lib/src/main/java/com/otaliastudios/transcoder/common/WavFile.java
 create mode 100644 lib/src/main/java/com/otaliastudios/transcoder/sink/RawDataSink.kt
 create mode 100644 lib/src/main/java/com/otaliastudios/transcoder/source/RawAudioDataSource.kt

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/common/WavFile.java b/lib/src/main/java/com/otaliastudios/transcoder/common/WavFile.java
new file mode 100644
index 00000000..847fe37a
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/common/WavFile.java
@@ -0,0 +1,302 @@
+package com.otaliastudios.transcoder.common;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class WavFile {
+    private enum IOState {READING, WRITING, CLOSED}
+
+    private final static int BUFFER_SIZE = 4096;
+
+    private final static int FMT_CHUNK_ID = 0x20746D66;
+    private final static int DATA_CHUNK_ID = 0x61746164;
+    private final static int RIFF_CHUNK_ID = 0x46464952;
+    private final static int RIFF_TYPE_ID = 0x45564157;
+
+    private File file;                        // File that will be read from or written to
+    private IOState ioState;                // Specifies the IO State of the Wav File (used for snaity checking)
+    private int bytesPerSample;            // Number of bytes required to store a single sample
+    private long numFrames;                    // Number of frames within the data section
+    private FileOutputStream oStream;    // Output stream used for writting data
+    private FileInputStream iStream;        // Input stream used for reading data
+    private float floatScale;                // Scaling factor used for int <-> float conversion
+    private float floatOffset;            // Offset factor used for int <-> float conversion
+    private boolean wordAlignAdjust;        // Specify if an extra byte at the end of the data chunk is required for word alignment
+
+    // Wav Header
+    private int numChannels;                // 2 bytes unsigned, 0x0001 (1) to 0xFFFF (65,535)
+    private long sampleRate;                // 4 bytes unsigned, 0x00000001 (1) to 0xFFFFFFFF (4,294,967,295)
+    // Although a java int is 4 bytes, it is signed, so need to use a long
+    private int blockAlign;                    // 2 bytes unsigned, 0x0001 (1) to 0xFFFF (65,535)
+    private int validBits;                    // 2 bytes unsigned, 0x0002 (2) to 0xFFFF (65,535)
+
+    // Buffering
+    private byte[] buffer;                    // Local buffer used for IO
+    private int bufferPointer;                // Points to the current position in local buffer
+    private int bytesRead;                    // Bytes read after last read into local buffer
+    private long frameCounter;                // Current number of frames read or written
+    private long fileSize;
+
+    // Cannot instantiate WavFile directly, must either use newWavFile() or openWavFile()
+    private WavFile() {
+        buffer = new byte[BUFFER_SIZE];
+    }
+
+    public int getNumChannels() {
+        return numChannels;
+    }
+
+    public long getNumFrames() {
+        return numFrames;
+    }
+
+    public long getFramesRemaining() {
+        return numFrames - frameCounter;
+    }
+
+    public long getSampleRate() {
+        return sampleRate;
+    }
+
+    public int getValidBits() {
+        return validBits;
+    }
+
+    public long getDuration() {
+        return getNumFrames() / getSampleRate();
+    }
+
+    public long getFileSize() {
+        return fileSize;
+    }
+
+    public static WavFile openWavFile(File file) throws Exception {
+        // Instantiate new Wavfile and store the file reference
+        WavFile wavFile = new WavFile();
+        wavFile.file = file;
+
+        // Create a new file input stream for reading file data
+        wavFile.iStream = new FileInputStream(file);
+
+        // Read the first 12 bytes of the file
+        int bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 12);
+        if (bytesRead != 12) throw new Exception("Not enough wav file bytes for header");
+
+        // Extract parts from the header
+        long riffChunkID = getLE(wavFile.buffer, 0, 4);
+        long chunkSize = getLE(wavFile.buffer, 4, 4);
+        long riffTypeID = getLE(wavFile.buffer, 8, 4);
+
+        // Check the header bytes contains the correct signature
+        if (riffChunkID != RIFF_CHUNK_ID)
+            throw new Exception("Invalid Wav Header data, incorrect riff chunk ID");
+        if (riffTypeID != RIFF_TYPE_ID) throw new Exception("Invalid Wav Header data, incorrect riff type ID");
+
+        // Check that the file size matches the number of bytes listed in header
+        if (file.length() != chunkSize + 8) {
+            throw new Exception("Header chunk size (" + chunkSize + ") does not match file size (" + file.length() + ")");
+        }
+
+        wavFile.fileSize = chunkSize;
+
+        boolean foundFormat = false;
+        boolean foundData = false;
+
+        // Search for the Format and Data Chunks
+        while (true) {
+            // Read the first 8 bytes of the chunk (ID and chunk size)
+            bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 8);
+            if (bytesRead == -1) throw new Exception("Reached end of file without finding format chunk");
+            if (bytesRead != 8) throw new Exception("Could not read chunk header");
+
+            // Extract the chunk ID and Size
+            long chunkID = getLE(wavFile.buffer, 0, 4);
+            chunkSize = getLE(wavFile.buffer, 4, 4);
+
+            // Word align the chunk size
+            // chunkSize specifies the number of bytes holding data. However,
+            // the data should be word aligned (2 bytes) so we need to calculate
+            // the actual number of bytes in the chunk
+            long numChunkBytes = (chunkSize % 2 == 1) ? chunkSize + 1 : chunkSize;
+
+            if (chunkID == FMT_CHUNK_ID) {
+                // Flag that the format chunk has been found
+                foundFormat = true;
+
+                // Read in the header info
+                bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 16);
+
+                // Check this is uncompressed data
+                int compressionCode = (int) getLE(wavFile.buffer, 0, 2);
+                if (compressionCode != 1)
+                    throw new Exception("Compression Code " + compressionCode + " not supported");
+
+                // Extract the format information
+                wavFile.numChannels = (int) getLE(wavFile.buffer, 2, 2);
+                wavFile.sampleRate = getLE(wavFile.buffer, 4, 4);
+                wavFile.blockAlign = (int) getLE(wavFile.buffer, 12, 2);
+                wavFile.validBits = (int) getLE(wavFile.buffer, 14, 2);
+
+                if (wavFile.numChannels == 0)
+                    throw new Exception("Number of channels specified in header is equal to zero");
+                if (wavFile.blockAlign == 0)
+                    throw new Exception("Block Align specified in header is equal to zero");
+                if (wavFile.validBits < 2) throw new Exception("Valid Bits specified in header is less than 2");
+                if (wavFile.validBits > 64)
+                    throw new Exception("Valid Bits specified in header is greater than 64, this is greater than a long can hold");
+
+                // Calculate the number of bytes required to hold 1 sample
+                wavFile.bytesPerSample = (wavFile.validBits + 7) / 8;
+                if (wavFile.bytesPerSample * wavFile.numChannels != wavFile.blockAlign)
+                    throw new Exception("Block Align does not agree with bytes required for validBits and number of channels");
+
+                // Account for number of format bytes and then skip over
+                // any extra format bytes
+                numChunkBytes -= 16;
+                if (numChunkBytes > 0) wavFile.iStream.skip(numChunkBytes);
+            } else if (chunkID == DATA_CHUNK_ID) {
+                // Check if we've found the format chunk,
+                // If not, throw an exception as we need the format information
+                // before we can read the data chunk
+                if (foundFormat == false) throw new Exception("Data chunk found before Format chunk");
+
+                // Check that the chunkSize (wav data length) is a multiple of the
+                // block align (bytes per frame)
+                if (chunkSize % wavFile.blockAlign != 0)
+                    throw new Exception("Data Chunk size is not multiple of Block Align");
+
+                // Calculate the number of frames
+                wavFile.numFrames = chunkSize / wavFile.blockAlign;
+
+                // Flag that we've found the wave data chunk
+                foundData = true;
+
+                break;
+            } else {
+                // If an unknown chunk ID is found, just skip over the chunk data
+                wavFile.iStream.skip(numChunkBytes);
+            }
+        }
+
+        // Throw an exception if no data chunk has been found
+        if (foundData == false) throw new Exception("Did not find a data chunk");
+
+        // Calculate the scaling factor for converting to a normalised double
+        if (wavFile.validBits > 8) {
+            // If more than 8 validBits, data is signed
+            // Conversion required dividing by magnitude of max negative value
+            wavFile.floatOffset = 0;
+            wavFile.floatScale = 1 << (wavFile.validBits - 1);
+        } else {
+            // Else if 8 or less validBits, data is unsigned
+            // Conversion required dividing by max positive value
+            wavFile.floatOffset = -1;
+            wavFile.floatScale = 0.5f * ((1 << wavFile.validBits) - 1);
+        }
+
+        wavFile.bufferPointer = 0;
+        wavFile.bytesRead = 0;
+        wavFile.frameCounter = 0;
+        wavFile.ioState = IOState.READING;
+
+        return wavFile;
+    }
+
+    private static long getLE(byte[] buffer, int pos, int numBytes) {
+        numBytes--;
+        pos += numBytes;
+
+        long val = buffer[pos] & 0xFF;
+        for (int b = 0; b < numBytes; b++) val = (val << 8) + (buffer[--pos] & 0xFF);
+
+        return val;
+    }
+
+    private long readSample() throws IOException, Exception {
+        long val = 0;
+
+        for (int b = 0; b < bytesPerSample; b++) {
+            if (bufferPointer == bytesRead) {
+                int read = iStream.read(buffer, 0, BUFFER_SIZE);
+                if (read == -1) throw new Exception("Not enough data available");
+                bytesRead = read;
+                bufferPointer = 0;
+            }
+
+            int v = buffer[bufferPointer];
+            if (b < bytesPerSample - 1 || bytesPerSample == 1) v &= 0xFF;
+            val += v << (b * 8);
+
+            bufferPointer++;
+        }
+
+        return val;
+    }
+
+    public int readFrames(float[] sampleBuffer, int numFramesToRead) throws IOException, Exception {
+        return readFramesInternal(sampleBuffer, 0, numFramesToRead);
+    }
+
+    private int readFramesInternal(float[] sampleBuffer, int offset, int numFramesToRead) throws IOException, Exception {
+        if (ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance");
+
+        for (int f = 0; f < numFramesToRead; f++) {
+            if (frameCounter == numFrames) return f;
+
+            for (int c = 0; c < numChannels; c++) {
+                sampleBuffer[offset] = floatOffset + (float) readSample() / floatScale;
+                offset++;
+            }
+
+            frameCounter++;
+        }
+
+        return numFramesToRead;
+    }
+
+    public int readFrames(int[] sampleBuffer, int numFramesToRead) throws IOException, Exception {
+        return readFramesInternal(sampleBuffer, 0, numFramesToRead);
+    }
+
+    private int readFramesInternal(int[] sampleBuffer, int offset, int numFramesToRead) throws IOException, Exception {
+        if (ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance");
+
+        for (int f = 0; f < numFramesToRead; f++) {
+            if (frameCounter == numFrames) return f;
+
+            for (int c = 0; c < numChannels; c++) {
+                sampleBuffer[offset] = (int) readSample();
+                offset++;
+            }
+
+            frameCounter++;
+        }
+
+        return numFramesToRead;
+    }
+
+    public void close() throws IOException {
+        // Close the input stream and set to null
+        if (iStream != null) {
+            iStream.close();
+            iStream = null;
+        }
+
+        if (oStream != null) {
+            // Write out anything still in the local buffer
+            if (bufferPointer > 0) oStream.write(buffer, 0, bufferPointer);
+
+            // If an extra byte is required for word alignment, add it to the end
+            if (wordAlignAdjust) oStream.write(0);
+
+            // Close the stream and set to null
+            oStream.close();
+            oStream = null;
+        }
+
+        // Flag that the stream is closed
+        ioState = IOState.CLOSED;
+    }
+}
\ No newline at end of file
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt
index ee8fa271..75c6f91f 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt
@@ -44,3 +44,14 @@ private class EosForcingDataSource(
         return force() || source.isDrained
     }
 }
+fun DataSource.ignoringEOS(force: () -> Boolean): DataSource =
+    EosIgnoringDataSource(this, force)
+
+private class EosIgnoringDataSource(
+    private val source: DataSource,
+    private val force: () -> Boolean,
+) : DataSource by source {
+    override fun isDrained(): Boolean {
+        return force() && source.isDrained
+    }
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/sink/RawDataSink.kt b/lib/src/main/java/com/otaliastudios/transcoder/sink/RawDataSink.kt
new file mode 100644
index 00000000..11d911d8
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/sink/RawDataSink.kt
@@ -0,0 +1,35 @@
+package com.otaliastudios.transcoder.sink
+
+import android.media.MediaCodec
+import android.media.MediaFormat
+import com.otaliastudios.transcoder.common.TrackStatus
+import com.otaliastudios.transcoder.common.TrackType
+import java.io.FileOutputStream
+import java.nio.ByteBuffer
+
+class RawDataSink(outputFilePath: String) : DataSink {
+
+    private var fileOutputStream: FileOutputStream = FileOutputStream(outputFilePath, true)
+
+    override fun setOrientation(orientation: Int) = Unit
+
+    override fun setLocation(latitude: Double, longitude: Double) = Unit
+
+    override fun setTrackStatus(type: TrackType, status: TrackStatus) = Unit
+
+    override fun setTrackFormat(type: TrackType, format: MediaFormat) = Unit
+
+    override fun writeTrack(
+        type: TrackType,
+        byteBuffer: ByteBuffer,
+        bufferInfo: MediaCodec.BufferInfo
+    ) {
+        fileOutputStream.write(byteBuffer.array(), 0, byteBuffer.limit())
+    }
+
+    override fun stop() = Unit
+
+    override fun release() {
+        fileOutputStream.close()
+    }
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/RawAudioDataSource.kt b/lib/src/main/java/com/otaliastudios/transcoder/source/RawAudioDataSource.kt
new file mode 100644
index 00000000..821dae2a
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/RawAudioDataSource.kt
@@ -0,0 +1,102 @@
+@file:Suppress("EmptyFunctionBlock", "MagicNumber", "TooManyFunctions")
+
+package com.otaliastudios.transcoder.source
+
+import android.media.MediaFormat
+import com.otaliastudios.transcoder.common.TrackType
+import com.otaliastudios.transcoder.common.WavFile
+import com.otaliastudios.transcoder.internal.audio.bitRate
+import com.otaliastudios.transcoder.internal.audio.samplesToBytes
+import com.otaliastudios.transcoder.internal.media.MediaFormatConstants
+import java.io.File
+import java.io.FileInputStream
+import java.io.IOException
+
+class RawAudioDataSource(private val path: String) : DataSource {
+
+    private var fileInputStream: FileInputStream? = null
+    private var mInitialized = false
+    val mediaFormat = MediaFormat()
+    private val b = ByteArray(samplesToBytes(1024, 2))
+    private val wavFile = WavFile.openWavFile(File(path))
+    private var frameCount: Int = 0
+
+    override fun initialize() {
+        fileInputStream = FileInputStream(path)
+        mInitialized = true
+    }
+
+    override fun deinitialize() {
+        mInitialized = false
+    }
+
+    override fun isInitialized() = mInitialized
+
+    override fun getOrientation() = 0
+
+    override fun getDurationUs() = 10000000L
+
+    override fun getLocation() = doubleArrayOf(0.0, 0.0)
+
+    override fun selectTrack(type: TrackType) {}
+
+    override fun seekTo(desiredPositionUs: Long) = 0L
+
+    override fun canReadTrack(type: TrackType): Boolean {
+        fileInputStream?.let {
+            return it.available() > 0
+        }
+        return false
+    }
+
+    var totalRead = 0
+    override fun readTrack(chunk: DataSource.Chunk) {
+        chunk.buffer.position(0)
+        var read = 0
+        try {
+            read = fileInputStream?.read(b)!!
+        } catch (e: IOException) {
+            println(e)
+        }
+        totalRead += read
+        val timestampUs = totalRead * 1000L * 1000L / (wavFile.sampleRate * wavFile.numChannels * BYTES_PER_SAMPLE)
+//        val timestampUs = frameCount * 1000L * 1000L * SAMPLES_PER_FRAME / wavFile.sampleRate.toFloat()
+
+        chunk.buffer.put(b, 0, read)
+        chunk.buffer.position(0)
+        chunk.buffer.limit(read)
+        chunk.timeUs = timestampUs
+        chunk.render = true
+        frameCount++
+    }
+
+    override fun getPositionUs() = 0L
+
+    override fun isDrained(): Boolean {
+        fileInputStream?.let { return it.available() <= 0 }
+        return true
+    }
+
+    override fun getTrackFormat(type: TrackType): MediaFormat {
+        return mediaFormat
+    }
+
+    override fun releaseTrack(type: TrackType) {}
+
+    init {
+        mediaFormat.setString(MediaFormat.KEY_MIME, MediaFormatConstants.MIMETYPE_AUDIO_RAW)
+        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate(wavFile.sampleRate.toInt(), wavFile.numChannels))
+        mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, wavFile.numChannels)
+        mediaFormat.setInteger(
+            MediaFormat.KEY_MAX_INPUT_SIZE,
+            samplesToBytes(BYTES_PER_SAMPLE * SAMPLES_PER_FRAME, wavFile.numChannels)
+        )
+        mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, wavFile.sampleRate.toInt())
+    }
+
+    companion object {
+        // AAC frame size. Audio encoder input size is a multiple of this
+        private const val SAMPLES_PER_FRAME = 1024
+        private const val BYTES_PER_SAMPLE = 2
+    }
+}

From 1ef84ba3b17499c008a6e7e94a7ee173d41d0ece Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Wed, 15 Sep 2021 13:23:45 +0530
Subject: [PATCH 035/101] Revert "wave file reader and header updater along
 with a raw audio data source"

This reverts commit c8882c7ee1d5bd7addee0521a72d0a7aba20212e.
---
 .../transcoder/common/WavFile.java            | 302 ------------------
 .../transcoder/internal/utils/eos.kt          |  11 -
 .../transcoder/sink/RawDataSink.kt            |  35 --
 .../transcoder/source/RawAudioDataSource.kt   | 102 ------
 4 files changed, 450 deletions(-)
 delete mode 100644 lib/src/main/java/com/otaliastudios/transcoder/common/WavFile.java
 delete mode 100644 lib/src/main/java/com/otaliastudios/transcoder/sink/RawDataSink.kt
 delete mode 100644 lib/src/main/java/com/otaliastudios/transcoder/source/RawAudioDataSource.kt

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/common/WavFile.java b/lib/src/main/java/com/otaliastudios/transcoder/common/WavFile.java
deleted file mode 100644
index 847fe37a..00000000
--- a/lib/src/main/java/com/otaliastudios/transcoder/common/WavFile.java
+++ /dev/null
@@ -1,302 +0,0 @@
-package com.otaliastudios.transcoder.common;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-
-public class WavFile {
-    private enum IOState {READING, WRITING, CLOSED}
-
-    private final static int BUFFER_SIZE = 4096;
-
-    private final static int FMT_CHUNK_ID = 0x20746D66;
-    private final static int DATA_CHUNK_ID = 0x61746164;
-    private final static int RIFF_CHUNK_ID = 0x46464952;
-    private final static int RIFF_TYPE_ID = 0x45564157;
-
-    private File file;                        // File that will be read from or written to
-    private IOState ioState;                // Specifies the IO State of the Wav File (used for snaity checking)
-    private int bytesPerSample;            // Number of bytes required to store a single sample
-    private long numFrames;                    // Number of frames within the data section
-    private FileOutputStream oStream;    // Output stream used for writting data
-    private FileInputStream iStream;        // Input stream used for reading data
-    private float floatScale;                // Scaling factor used for int <-> float conversion
-    private float floatOffset;            // Offset factor used for int <-> float conversion
-    private boolean wordAlignAdjust;        // Specify if an extra byte at the end of the data chunk is required for word alignment
-
-    // Wav Header
-    private int numChannels;                // 2 bytes unsigned, 0x0001 (1) to 0xFFFF (65,535)
-    private long sampleRate;                // 4 bytes unsigned, 0x00000001 (1) to 0xFFFFFFFF (4,294,967,295)
-    // Although a java int is 4 bytes, it is signed, so need to use a long
-    private int blockAlign;                    // 2 bytes unsigned, 0x0001 (1) to 0xFFFF (65,535)
-    private int validBits;                    // 2 bytes unsigned, 0x0002 (2) to 0xFFFF (65,535)
-
-    // Buffering
-    private byte[] buffer;                    // Local buffer used for IO
-    private int bufferPointer;                // Points to the current position in local buffer
-    private int bytesRead;                    // Bytes read after last read into local buffer
-    private long frameCounter;                // Current number of frames read or written
-    private long fileSize;
-
-    // Cannot instantiate WavFile directly, must either use newWavFile() or openWavFile()
-    private WavFile() {
-        buffer = new byte[BUFFER_SIZE];
-    }
-
-    public int getNumChannels() {
-        return numChannels;
-    }
-
-    public long getNumFrames() {
-        return numFrames;
-    }
-
-    public long getFramesRemaining() {
-        return numFrames - frameCounter;
-    }
-
-    public long getSampleRate() {
-        return sampleRate;
-    }
-
-    public int getValidBits() {
-        return validBits;
-    }
-
-    public long getDuration() {
-        return getNumFrames() / getSampleRate();
-    }
-
-    public long getFileSize() {
-        return fileSize;
-    }
-
-    public static WavFile openWavFile(File file) throws Exception {
-        // Instantiate new Wavfile and store the file reference
-        WavFile wavFile = new WavFile();
-        wavFile.file = file;
-
-        // Create a new file input stream for reading file data
-        wavFile.iStream = new FileInputStream(file);
-
-        // Read the first 12 bytes of the file
-        int bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 12);
-        if (bytesRead != 12) throw new Exception("Not enough wav file bytes for header");
-
-        // Extract parts from the header
-        long riffChunkID = getLE(wavFile.buffer, 0, 4);
-        long chunkSize = getLE(wavFile.buffer, 4, 4);
-        long riffTypeID = getLE(wavFile.buffer, 8, 4);
-
-        // Check the header bytes contains the correct signature
-        if (riffChunkID != RIFF_CHUNK_ID)
-            throw new Exception("Invalid Wav Header data, incorrect riff chunk ID");
-        if (riffTypeID != RIFF_TYPE_ID) throw new Exception("Invalid Wav Header data, incorrect riff type ID");
-
-        // Check that the file size matches the number of bytes listed in header
-        if (file.length() != chunkSize + 8) {
-            throw new Exception("Header chunk size (" + chunkSize + ") does not match file size (" + file.length() + ")");
-        }
-
-        wavFile.fileSize = chunkSize;
-
-        boolean foundFormat = false;
-        boolean foundData = false;
-
-        // Search for the Format and Data Chunks
-        while (true) {
-            // Read the first 8 bytes of the chunk (ID and chunk size)
-            bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 8);
-            if (bytesRead == -1) throw new Exception("Reached end of file without finding format chunk");
-            if (bytesRead != 8) throw new Exception("Could not read chunk header");
-
-            // Extract the chunk ID and Size
-            long chunkID = getLE(wavFile.buffer, 0, 4);
-            chunkSize = getLE(wavFile.buffer, 4, 4);
-
-            // Word align the chunk size
-            // chunkSize specifies the number of bytes holding data. However,
-            // the data should be word aligned (2 bytes) so we need to calculate
-            // the actual number of bytes in the chunk
-            long numChunkBytes = (chunkSize % 2 == 1) ? chunkSize + 1 : chunkSize;
-
-            if (chunkID == FMT_CHUNK_ID) {
-                // Flag that the format chunk has been found
-                foundFormat = true;
-
-                // Read in the header info
-                bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 16);
-
-                // Check this is uncompressed data
-                int compressionCode = (int) getLE(wavFile.buffer, 0, 2);
-                if (compressionCode != 1)
-                    throw new Exception("Compression Code " + compressionCode + " not supported");
-
-                // Extract the format information
-                wavFile.numChannels = (int) getLE(wavFile.buffer, 2, 2);
-                wavFile.sampleRate = getLE(wavFile.buffer, 4, 4);
-                wavFile.blockAlign = (int) getLE(wavFile.buffer, 12, 2);
-                wavFile.validBits = (int) getLE(wavFile.buffer, 14, 2);
-
-                if (wavFile.numChannels == 0)
-                    throw new Exception("Number of channels specified in header is equal to zero");
-                if (wavFile.blockAlign == 0)
-                    throw new Exception("Block Align specified in header is equal to zero");
-                if (wavFile.validBits < 2) throw new Exception("Valid Bits specified in header is less than 2");
-                if (wavFile.validBits > 64)
-                    throw new Exception("Valid Bits specified in header is greater than 64, this is greater than a long can hold");
-
-                // Calculate the number of bytes required to hold 1 sample
-                wavFile.bytesPerSample = (wavFile.validBits + 7) / 8;
-                if (wavFile.bytesPerSample * wavFile.numChannels != wavFile.blockAlign)
-                    throw new Exception("Block Align does not agree with bytes required for validBits and number of channels");
-
-                // Account for number of format bytes and then skip over
-                // any extra format bytes
-                numChunkBytes -= 16;
-                if (numChunkBytes > 0) wavFile.iStream.skip(numChunkBytes);
-            } else if (chunkID == DATA_CHUNK_ID) {
-                // Check if we've found the format chunk,
-                // If not, throw an exception as we need the format information
-                // before we can read the data chunk
-                if (foundFormat == false) throw new Exception("Data chunk found before Format chunk");
-
-                // Check that the chunkSize (wav data length) is a multiple of the
-                // block align (bytes per frame)
-                if (chunkSize % wavFile.blockAlign != 0)
-                    throw new Exception("Data Chunk size is not multiple of Block Align");
-
-                // Calculate the number of frames
-                wavFile.numFrames = chunkSize / wavFile.blockAlign;
-
-                // Flag that we've found the wave data chunk
-                foundData = true;
-
-                break;
-            } else {
-                // If an unknown chunk ID is found, just skip over the chunk data
-                wavFile.iStream.skip(numChunkBytes);
-            }
-        }
-
-        // Throw an exception if no data chunk has been found
-        if (foundData == false) throw new Exception("Did not find a data chunk");
-
-        // Calculate the scaling factor for converting to a normalised double
-        if (wavFile.validBits > 8) {
-            // If more than 8 validBits, data is signed
-            // Conversion required dividing by magnitude of max negative value
-            wavFile.floatOffset = 0;
-            wavFile.floatScale = 1 << (wavFile.validBits - 1);
-        } else {
-            // Else if 8 or less validBits, data is unsigned
-            // Conversion required dividing by max positive value
-            wavFile.floatOffset = -1;
-            wavFile.floatScale = 0.5f * ((1 << wavFile.validBits) - 1);
-        }
-
-        wavFile.bufferPointer = 0;
-        wavFile.bytesRead = 0;
-        wavFile.frameCounter = 0;
-        wavFile.ioState = IOState.READING;
-
-        return wavFile;
-    }
-
-    private static long getLE(byte[] buffer, int pos, int numBytes) {
-        numBytes--;
-        pos += numBytes;
-
-        long val = buffer[pos] & 0xFF;
-        for (int b = 0; b < numBytes; b++) val = (val << 8) + (buffer[--pos] & 0xFF);
-
-        return val;
-    }
-
-    private long readSample() throws IOException, Exception {
-        long val = 0;
-
-        for (int b = 0; b < bytesPerSample; b++) {
-            if (bufferPointer == bytesRead) {
-                int read = iStream.read(buffer, 0, BUFFER_SIZE);
-                if (read == -1) throw new Exception("Not enough data available");
-                bytesRead = read;
-                bufferPointer = 0;
-            }
-
-            int v = buffer[bufferPointer];
-            if (b < bytesPerSample - 1 || bytesPerSample == 1) v &= 0xFF;
-            val += v << (b * 8);
-
-            bufferPointer++;
-        }
-
-        return val;
-    }
-
-    public int readFrames(float[] sampleBuffer, int numFramesToRead) throws IOException, Exception {
-        return readFramesInternal(sampleBuffer, 0, numFramesToRead);
-    }
-
-    private int readFramesInternal(float[] sampleBuffer, int offset, int numFramesToRead) throws IOException, Exception {
-        if (ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance");
-
-        for (int f = 0; f < numFramesToRead; f++) {
-            if (frameCounter == numFrames) return f;
-
-            for (int c = 0; c < numChannels; c++) {
-                sampleBuffer[offset] = floatOffset + (float) readSample() / floatScale;
-                offset++;
-            }
-
-            frameCounter++;
-        }
-
-        return numFramesToRead;
-    }
-
-    public int readFrames(int[] sampleBuffer, int numFramesToRead) throws IOException, Exception {
-        return readFramesInternal(sampleBuffer, 0, numFramesToRead);
-    }
-
-    private int readFramesInternal(int[] sampleBuffer, int offset, int numFramesToRead) throws IOException, Exception {
-        if (ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance");
-
-        for (int f = 0; f < numFramesToRead; f++) {
-            if (frameCounter == numFrames) return f;
-
-            for (int c = 0; c < numChannels; c++) {
-                sampleBuffer[offset] = (int) readSample();
-                offset++;
-            }
-
-            frameCounter++;
-        }
-
-        return numFramesToRead;
-    }
-
-    public void close() throws IOException {
-        // Close the input stream and set to null
-        if (iStream != null) {
-            iStream.close();
-            iStream = null;
-        }
-
-        if (oStream != null) {
-            // Write out anything still in the local buffer
-            if (bufferPointer > 0) oStream.write(buffer, 0, bufferPointer);
-
-            // If an extra byte is required for word alignment, add it to the end
-            if (wordAlignAdjust) oStream.write(0);
-
-            // Close the stream and set to null
-            oStream.close();
-            oStream = null;
-        }
-
-        // Flag that the stream is closed
-        ioState = IOState.CLOSED;
-    }
-}
\ No newline at end of file
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt
index 75c6f91f..ee8fa271 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt
@@ -44,14 +44,3 @@ private class EosForcingDataSource(
         return force() || source.isDrained
     }
 }
-fun DataSource.ignoringEOS(force: () -> Boolean): DataSource =
-    EosIgnoringDataSource(this, force)
-
-private class EosIgnoringDataSource(
-    private val source: DataSource,
-    private val force: () -> Boolean,
-) : DataSource by source {
-    override fun isDrained(): Boolean {
-        return force() && source.isDrained
-    }
-}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/sink/RawDataSink.kt b/lib/src/main/java/com/otaliastudios/transcoder/sink/RawDataSink.kt
deleted file mode 100644
index 11d911d8..00000000
--- a/lib/src/main/java/com/otaliastudios/transcoder/sink/RawDataSink.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.otaliastudios.transcoder.sink
-
-import android.media.MediaCodec
-import android.media.MediaFormat
-import com.otaliastudios.transcoder.common.TrackStatus
-import com.otaliastudios.transcoder.common.TrackType
-import java.io.FileOutputStream
-import java.nio.ByteBuffer
-
-class RawDataSink(outputFilePath: String) : DataSink {
-
-    private var fileOutputStream: FileOutputStream = FileOutputStream(outputFilePath, true)
-
-    override fun setOrientation(orientation: Int) = Unit
-
-    override fun setLocation(latitude: Double, longitude: Double) = Unit
-
-    override fun setTrackStatus(type: TrackType, status: TrackStatus) = Unit
-
-    override fun setTrackFormat(type: TrackType, format: MediaFormat) = Unit
-
-    override fun writeTrack(
-        type: TrackType,
-        byteBuffer: ByteBuffer,
-        bufferInfo: MediaCodec.BufferInfo
-    ) {
-        fileOutputStream.write(byteBuffer.array(), 0, byteBuffer.limit())
-    }
-
-    override fun stop() = Unit
-
-    override fun release() {
-        fileOutputStream.close()
-    }
-}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/RawAudioDataSource.kt b/lib/src/main/java/com/otaliastudios/transcoder/source/RawAudioDataSource.kt
deleted file mode 100644
index 821dae2a..00000000
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/RawAudioDataSource.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-@file:Suppress("EmptyFunctionBlock", "MagicNumber", "TooManyFunctions")
-
-package com.otaliastudios.transcoder.source
-
-import android.media.MediaFormat
-import com.otaliastudios.transcoder.common.TrackType
-import com.otaliastudios.transcoder.common.WavFile
-import com.otaliastudios.transcoder.internal.audio.bitRate
-import com.otaliastudios.transcoder.internal.audio.samplesToBytes
-import com.otaliastudios.transcoder.internal.media.MediaFormatConstants
-import java.io.File
-import java.io.FileInputStream
-import java.io.IOException
-
-class RawAudioDataSource(private val path: String) : DataSource {
-
-    private var fileInputStream: FileInputStream? = null
-    private var mInitialized = false
-    val mediaFormat = MediaFormat()
-    private val b = ByteArray(samplesToBytes(1024, 2))
-    private val wavFile = WavFile.openWavFile(File(path))
-    private var frameCount: Int = 0
-
-    override fun initialize() {
-        fileInputStream = FileInputStream(path)
-        mInitialized = true
-    }
-
-    override fun deinitialize() {
-        mInitialized = false
-    }
-
-    override fun isInitialized() = mInitialized
-
-    override fun getOrientation() = 0
-
-    override fun getDurationUs() = 10000000L
-
-    override fun getLocation() = doubleArrayOf(0.0, 0.0)
-
-    override fun selectTrack(type: TrackType) {}
-
-    override fun seekTo(desiredPositionUs: Long) = 0L
-
-    override fun canReadTrack(type: TrackType): Boolean {
-        fileInputStream?.let {
-            return it.available() > 0
-        }
-        return false
-    }
-
-    var totalRead = 0
-    override fun readTrack(chunk: DataSource.Chunk) {
-        chunk.buffer.position(0)
-        var read = 0
-        try {
-            read = fileInputStream?.read(b)!!
-        } catch (e: IOException) {
-            println(e)
-        }
-        totalRead += read
-        val timestampUs = totalRead * 1000L * 1000L / (wavFile.sampleRate * wavFile.numChannels * BYTES_PER_SAMPLE)
-//        val timestampUs = frameCount * 1000L * 1000L * SAMPLES_PER_FRAME / wavFile.sampleRate.toFloat()
-
-        chunk.buffer.put(b, 0, read)
-        chunk.buffer.position(0)
-        chunk.buffer.limit(read)
-        chunk.timeUs = timestampUs
-        chunk.render = true
-        frameCount++
-    }
-
-    override fun getPositionUs() = 0L
-
-    override fun isDrained(): Boolean {
-        fileInputStream?.let { return it.available() <= 0 }
-        return true
-    }
-
-    override fun getTrackFormat(type: TrackType): MediaFormat {
-        return mediaFormat
-    }
-
-    override fun releaseTrack(type: TrackType) {}
-
-    init {
-        mediaFormat.setString(MediaFormat.KEY_MIME, MediaFormatConstants.MIMETYPE_AUDIO_RAW)
-        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate(wavFile.sampleRate.toInt(), wavFile.numChannels))
-        mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, wavFile.numChannels)
-        mediaFormat.setInteger(
-            MediaFormat.KEY_MAX_INPUT_SIZE,
-            samplesToBytes(BYTES_PER_SAMPLE * SAMPLES_PER_FRAME, wavFile.numChannels)
-        )
-        mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, wavFile.sampleRate.toInt())
-    }
-
-    companion object {
-        // AAC frame size. Audio encoder input size is a multiple of this
-        private const val SAMPLES_PER_FRAME = 1024
-        private const val BYTES_PER_SAMPLE = 2
-    }
-}

From 2a96f2801a7f6cf7f8f9e51ff40a8bf2bad33fa1 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Thu, 30 Sep 2021 14:11:02 +0530
Subject: [PATCH 036/101] pending range logic fix

---
 .../otaliastudios/transcoder/internal/codec/DecoderDropper.kt | 4 ++--
 .../transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
index 1b238b10..6f6d6944 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt
@@ -48,8 +48,8 @@ class DecoderDropper(private val continuous: Boolean) {
         if (render) {
             debug("INPUT: inputUs=$timeUs")
             // log.v("TDBG inputUs=$timeUs")
-            if (pendingRange == null) pendingRange = timeUs..Long.MAX_VALUE
-            else pendingRange = pendingRange!!.first..timeUs
+            pendingRange = if (pendingRange == null) timeUs..Long.MAX_VALUE
+            else pendingRange!!.first.coerceAtMost(timeUs)..timeUs
         } else {
             debug("INPUT: Got SKIPPING input! inputUs=$timeUs")
             if (pendingRange != null && pendingRange!!.last != Long.MAX_VALUE) {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index b4412ccf..d52eb5fc 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -85,10 +85,10 @@ class DefaultThumbnailsEngine(
         val source = dataSources[type][index]
         return Pipeline.build("Thumbnails") {
             Seeker(source, fetchPosition) {
-                val nextSeek = (ceil(source.getPositionUs().toDouble() / KEY_FRAME) * KEY_FRAME).toLong()
+                val nextSeek = (ceil(source.positionUs.toDouble() / KEY_FRAME) * KEY_FRAME).toLong()
                 val position = stubs.firstOrNull()?.positionUs ?: -1
 
-                val seek = !(position > previousSnapshotUs && position < nextSeek) && shouldSeek
+                val seek = position !in (previousSnapshotUs + 1) until nextSeek && shouldSeek
 
                 if (seek) {
                     shouldFlush = true

From 3be764d39dfad76f61364f31f2d6b22ec7fbfb66 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Thu, 23 Dec 2021 23:30:19 +0530
Subject: [PATCH 037/101] processed timestamp callback from writer step

---
 .../java/com/otaliastudios/transcoder/internal/data/Writer.kt | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
index ba875bef..9ecde76e 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
@@ -23,7 +23,8 @@ interface WriterChannel : Channel {
 
 class Writer(
     private val sink: DataSink,
-    private val track: TrackType
+    private val track: TrackType,
+    private val processedTimeStamp: ((Long) -> Unit)? = null
 ) : Step<WriterData, WriterChannel, Unit, Channel>, WriterChannel {
 
     override val channel = this
@@ -48,6 +49,7 @@ class Writer(
             } else flags
         )
         sink.writeTrack(track, buffer, info)
+        if (!eos) processedTimeStamp?.invoke(timestamp)
         state.value.release()
         return if (eos) State.Eos(Unit) else State.Ok(Unit)
     }

From b832d68e1a5bdd6744c1f753a66f13495070109e Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Wed, 29 Dec 2021 18:14:45 +0530
Subject: [PATCH 038/101] send -1 in callback if EOS

---
 .../java/com/otaliastudios/transcoder/internal/data/Writer.kt   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
index 9ecde76e..40448bab 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
@@ -49,7 +49,7 @@ class Writer(
             } else flags
         )
         sink.writeTrack(track, buffer, info)
-        if (!eos) processedTimeStamp?.invoke(timestamp)
+        processedTimeStamp?.invoke(if (!eos) timestamp else -1)
         state.value.release()
         return if (eos) State.Eos(Unit) else State.Ok(Unit)
     }

From cad31b1ddc12665c9caaf487f1828788c9ec3846 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Tue, 18 Jan 2022 18:29:18 +0530
Subject: [PATCH 039/101] pass UI threshold to thumbnailer and optimise seeking

---
 .../transcoder/internal/data/Seeker.kt        |  8 +-
 .../thumbnails/DefaultThumbnailsEngine.kt     | 80 ++++++++++++-------
 .../internal/video/VideoSnapshots.kt          | 12 +--
 .../transcoder/source/DataSource.java         |  4 +
 .../transcoder/source/DefaultDataSource.java  | 32 +++++++-
 .../thumbnail/SingleThumbnailRequest.kt       |  4 +-
 .../transcoder/thumbnail/ThumbnailRequest.kt  |  1 +
 7 files changed, 96 insertions(+), 45 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
index a22cc953..fa86baf3 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt
@@ -8,16 +8,16 @@ import com.otaliastudios.transcoder.source.DataSource
 
 class Seeker(
     private val source: DataSource,
-    private val fetchPosition: () -> Long?,
-    private val shouldSeek: (Long) -> Boolean
+    private val shouldSeek: () -> Pair<Long, Boolean>
 ) : BaseStep<Unit, Channel, Unit, Channel>() {
 
     private val log = Logger("Seeker")
     override val channel = Channel
 
     override fun step(state: State.Ok<Unit>, fresh: Boolean): State<Unit> {
-        val position = fetchPosition()
-        if (position != null && shouldSeek(position)) {
+        val shouldSeek = shouldSeek()
+        val position = shouldSeek.first
+        if (shouldSeek.second) {
             log.i("Seeking to next position $position where currentReaderTime=${source.getPositionUs()}")
             source.seekTo(position)
         } else {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index d52eb5fc..0dea6d73 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -27,7 +27,6 @@ import com.otaliastudios.transcoder.time.DefaultTimeInterpolator
 import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.isActive
-import kotlin.math.ceil
 
 class DefaultThumbnailsEngine(
     private val dataSources: DataSources,
@@ -84,18 +83,34 @@ class DefaultThumbnailsEngine(
     ): Pipeline {
         val source = dataSources[type][index]
         return Pipeline.build("Thumbnails") {
-            Seeker(source, fetchPosition) {
-                val nextSeek = (ceil(source.positionUs.toDouble() / KEY_FRAME) * KEY_FRAME).toLong()
-                val position = stubs.firstOrNull()?.positionUs ?: -1
-
-                val seek = position !in (previousSnapshotUs + 1) until nextSeek && shouldSeek
-
-                if (seek) {
-                    shouldFlush = true
-                    shouldSeek = false
-                }
-                seek
-                //                log.i("Seeker state check: $it :$stubs")
+            Seeker(source) {
+                var seek = false
+                var seekUs = 0L
+                val current = source.positionUs
+                val requested = stubs.firstOrNull()?.positionUs ?: -1
+                val threshold = stubs.firstOrNull()?.request?.threshold() ?: 0L
+                val nextKeyFrameIndex = source.keyFrameTimestampsUs.indexOfFirst { it >= requested }
+                val nextKeyFrameUs =
+                    if (nextKeyFrameIndex == -1) -1L else source.keyFrameTimestampsUs[nextKeyFrameIndex]
+                val previousKeyFrameUs =
+                    if (nextKeyFrameIndex > 0) source.keyFrameTimestampsUs[nextKeyFrameIndex - 1] else 0
+
+                if (nextKeyFrameUs == -1L || !shouldSeek || requested == -1L) // no keyframe found, so can't seek.
+                    return@Seeker Pair(seekUs, seek)
+
+                log.i(
+                    "seek: current ${source.positionUs}," +
+                        " requested $requested, threshold $threshold, nextKeyFrameUs $nextKeyFrameUs"
+                )
+                
+                val rightGap = nextKeyFrameUs - requested
+                val nextKeyFrameInThreshold = rightGap <= threshold
+                seek = nextKeyFrameInThreshold || previousKeyFrameUs > current || (current - requested > threshold)
+                seekUs = if (nextKeyFrameInThreshold) nextKeyFrameUs else previousKeyFrameUs
+
+                shouldFlush = seek
+                shouldSeek = false
+                Pair(seekUs, seek)
             } +
                 Reader(source, type) +
                 Decoder(source.getTrackFormat(type)!!, continuous = false) {
@@ -106,19 +121,21 @@ class DefaultThumbnailsEngine(
                 VideoRenderer(source.orientation, rotation, outputFormat, flipY = true, true) {
                     stubs.firstOrNull()?.positionUs ?: -1
                 } +
-                VideoSnapshots(outputFormat, fetchPosition, snapshotAccuracyUs) { pos, bitmap ->
-                    shouldSeek = true
-                    val stub = stubs.removeFirst()
-                    stub.actualLocalizedUs = pos
-                    previousSnapshotUs = pos
-                    log.i(
-                        "Got snapshot. positionUs=${stub.positionUs} " +
-                            "localizedUs=${stub.localizedUs} " +
-                            "actualLocalizedUs=${stub.actualLocalizedUs} " +
-                            "deltaUs=${stub.localizedUs - stub.actualLocalizedUs}"
-                    )
-                    val thumbnail = Thumbnail(stub.request, stub.positionUs, bitmap)
-                    progress(thumbnail)
+                VideoSnapshots(outputFormat, fetchPosition) { pos, bitmap ->
+                    val stub = stubs.removeFirstOrNull()
+                    if (stub != null) {
+                        shouldSeek = true
+                        stub.actualLocalizedUs = pos
+                        previousSnapshotUs = pos
+                        log.i(
+                            "Got snapshot. positionUs=${stub.positionUs} " +
+                                "localizedUs=${stub.localizedUs} " +
+                                "actualLocalizedUs=${stub.actualLocalizedUs} " +
+                                "deltaUs=${stub.localizedUs - stub.actualLocalizedUs}"
+                        )
+                        val thumbnail = Thumbnail(stub.request, stub.positionUs, bitmap)
+                        progress(thumbnail)
+                    }
                 }
         }
     }
@@ -156,6 +173,7 @@ class DefaultThumbnailsEngine(
         if (stub != null) {
             log.i("removePosition Match: $positionUs :$stubs")
             stubs.remove(stub)
+            shouldSeek = true
         }
     }
 
@@ -174,7 +192,10 @@ class DefaultThumbnailsEngine(
         )
     }
 
-    private val fetchPosition: () -> Long? = { stubs.firstOrNull()?.localizedUs }
+    private val fetchPosition: () -> VideoSnapshots.Request? = {
+        if (stubs.isEmpty()) null
+        else VideoSnapshots.Request(stubs.first().localizedUs, stubs.first().request.threshold())
+    }
 
     override fun cleanup() {
         runCatching { segments.release() }
@@ -182,9 +203,6 @@ class DefaultThumbnailsEngine(
     }
 
     companion object {
-        private const val WAIT_MS = 10L
-        private const val PROGRESS_LOOPS = 10L
-        private const val snapshotAccuracyUs = 500 * 1000L
-        private const val KEY_FRAME = 1000 * 1000L
+        private const val WAIT_MS = 5L
     }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt
index a6f5cca0..74b61563 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt
@@ -17,12 +17,10 @@ import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.internal.utils.Logger
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
-import kotlin.math.abs
 
 class VideoSnapshots(
     format: MediaFormat,
-    private val fetchRequest: () -> Long?,
-    private val accuracyUs: Long,
+    private val fetchRequest: () -> Request?,
     private val onSnapshot: (Long, Bitmap) -> Unit
 ) : BaseStep<Long, Channel, Long, Channel>() {
 
@@ -36,9 +34,10 @@ class VideoSnapshots(
     }
 
     override fun step(state: State.Ok<Long>, fresh: Boolean): State<Long> {
-        val expectedUs = fetchRequest() ?: return state
-
-        val deltaUs = abs(expectedUs - state.value)
+        val request = fetchRequest() ?: return state
+        val expectedUs = request.pts
+        val accuracyUs = request.threshold
+        val deltaUs = (expectedUs - state.value)
         if (deltaUs < accuracyUs || (state is State.Eos && expectedUs > state.value)) {
             log.i("Request MATCHED! expectedUs=$expectedUs actualUs=${state.value} deltaUs=$deltaUs")
             val buffer = ByteBuffer.allocateDirect(width * height * 4)
@@ -59,4 +58,5 @@ class VideoSnapshots(
         surface.release()
         core.release()
     }
+    data class Request(val pts: Long, val threshold: Long)
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
index 963e76d5..c514b59b 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
@@ -8,6 +8,7 @@
 import com.otaliastudios.transcoder.common.TrackType;
 
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
 
 /**
  * Represents the source of input data.
@@ -111,6 +112,9 @@ public interface DataSource {
      */
     void releaseTrack(@NonNull TrackType type);
 
+    default ArrayList<Long> getKeyFrameTimestampsUs() {
+        return new ArrayList<>();
+    }
     /**
      * Rewinds this source, moving it to its default state.
      * To be used again, tracks will be selected again.
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index 70e88ceb..18baf747 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -14,6 +14,7 @@
 import com.otaliastudios.transcoder.internal.utils.MutableTrackMap;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -43,6 +44,7 @@ public abstract class DefaultDataSource implements DataSource {
     private long mDontRenderRangeStart = -1L;
     private long mDontRenderRangeEnd = -1L;
 
+    private final ArrayList<Long> keyFrameTsUs = new ArrayList<>();
     @Override
     public void initialize() {
         LOG.i("initialize(): initializing...");
@@ -74,6 +76,10 @@ public void initialize() {
         for (int i = 0; i < mExtractor.getTrackCount(); i++) mExtractor.unselectTrack(i);
         mInitialized = true;
 
+        selectTrack(TrackType.VIDEO);
+        keyFrameTsUs.clear();
+        getSyncFrameTimestamps();
+        releaseTrack(TrackType.VIDEO);
         // Debugging mOriginUs issues.
         /* LOG.v("initialize(): origin after unselect is" + mExtractor.getSampleTime());
         if (getTrackFormat(TrackType.VIDEO) != null) {
@@ -88,6 +94,30 @@ public void initialize() {
         } */
     }
 
+    @Override
+    public ArrayList<Long> getKeyFrameTimestampsUs() {
+        return keyFrameTsUs;
+    }
+
+    private void getSyncFrameTimestamps() {
+        long lastSampleTime = -1L;
+        long sampleTime = mExtractor.getSampleTime();
+
+        LOG.i("start:" + sampleTime);
+
+        while (sampleTime >= 0L && sampleTime != lastSampleTime) {
+            if ((mExtractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) > 0) {
+                keyFrameTsUs.add(sampleTime);
+            }
+            mExtractor.seekTo(sampleTime + 1L, MediaExtractor.SEEK_TO_NEXT_SYNC);
+            lastSampleTime = sampleTime;
+            sampleTime = mExtractor.getSampleTime();
+        }
+        mExtractor.seekTo(0, MediaExtractor.SEEK_TO_NEXT_SYNC);
+        LOG.i("stop:" + keyFrameTsUs);
+    }
+
+
     @Override
     public void deinitialize() {
         LOG.i("deinitialize(): deinitializing...");
@@ -162,7 +192,7 @@ public long seekTo(long desiredPositionUs) {
             mExtractor.seekTo(mExtractor.getSampleTime(), MediaExtractor.SEEK_TO_CLOSEST_SYNC);
             LOG.v("seekTo(): seek workaround completed. (extractorUs=" + mExtractor.getSampleTime() + ")");
         } else {
-            mExtractor.seekTo(mOriginUs + desiredPositionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+            mExtractor.seekTo(desiredPositionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
         }
         mDontRenderRangeStart = mExtractor.getSampleTime();
         mDontRenderRangeEnd = mOriginUs + desiredPositionUs;
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
index d852f005..0411d6a3 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
@@ -2,9 +2,7 @@ package com.otaliastudios.transcoder.thumbnail
 
 open class SingleThumbnailRequest(private val positionUs: Long) : ThumbnailRequest {
     override fun locate(durationUs: Long): List<Long> {
-        require(positionUs in 0L..durationUs) {
-            "Thumbnail position is out of range. position=$positionUs range=${0L..durationUs}"
-        }
+        val positionUs = positionUs.coerceIn(0L..durationUs)
         return listOf(positionUs)
     }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt
index e2f19aed..8724b7ba 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt
@@ -3,6 +3,7 @@ package com.otaliastudios.transcoder.thumbnail
 interface ThumbnailRequest {
     fun locate(durationUs: Long): List<Long>
 
+    fun threshold(): Long = 0
     // Could make it so that if locate() is empty, accept is called for each frame (no seeking).
     // But this only makes sense if accept signature has more information (segment, ...), and
     // it should also have a way to say - we're done, stop transcoding.

From 79437a96bb45023d4e0aff35be9b88b574ef21c8 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Thu, 20 Jan 2022 16:50:27 +0530
Subject: [PATCH 040/101] wip

---
 .../thumbnails/DefaultThumbnailsEngine.kt     | 23 ++++++++++++++-
 .../transcoder/source/DataSource.java         |  5 ++++
 .../transcoder/source/DefaultDataSource.java  | 29 ++++++++++++++-----
 3 files changed, 48 insertions(+), 9 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 0dea6d73..b25dd27e 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -19,6 +19,8 @@ import com.otaliastudios.transcoder.internal.utils.trackMapOf
 import com.otaliastudios.transcoder.internal.video.VideoRenderer
 import com.otaliastudios.transcoder.internal.video.VideoSnapshots
 import com.otaliastudios.transcoder.resize.Resizer
+import com.otaliastudios.transcoder.source.DataSource
+import com.otaliastudios.transcoder.source.DefaultDataSource
 import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
 import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
@@ -89,7 +91,8 @@ class DefaultThumbnailsEngine(
                 val current = source.positionUs
                 val requested = stubs.firstOrNull()?.positionUs ?: -1
                 val threshold = stubs.firstOrNull()?.request?.threshold() ?: 0L
-                val nextKeyFrameIndex = source.keyFrameTimestampsUs.indexOfFirst { it >= requested }
+                val nextKeyFrameIndex = source.search(requested)
+
                 val nextKeyFrameUs =
                     if (nextKeyFrameIndex == -1) -1L else source.keyFrameTimestampsUs[nextKeyFrameIndex]
                 val previousKeyFrameUs =
@@ -142,6 +145,24 @@ class DefaultThumbnailsEngine(
 
     private lateinit var progress: (Thumbnail) -> Unit
 
+    private fun DataSource.search(timestampUs: Long) : Int  {
+        if(keyFrameTimestampsUs.isEmpty())
+            requestKeyFrameTimestamps()
+
+        val searchIndex = keyFrameTimestampsUs.binarySearch(timestampUs)
+
+        val nextKeyFrameIndex = when {
+                searchIndex >= 0 -> searchIndex
+                searchIndex == -keyFrameTimestampsUs.size -> {
+                    if (requestKeyFrameTimestamps() == -1L) -1
+                    else search(timestampUs)
+                }
+                else -> { -searchIndex - 1 } // searchIndex < 0
+            }
+
+        return nextKeyFrameIndex
+    }
+
     override suspend fun queueThumbnails(list: List<ThumbnailRequest>, progress: (Thumbnail) -> Unit) {
         val segment = segments.next(TrackType.VIDEO)
         segment?.let {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
index c514b59b..c6e2efe7 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
@@ -112,6 +112,8 @@ public interface DataSource {
      */
     void releaseTrack(@NonNull TrackType type);
 
+    default long requestKeyFrameTimestamps() { return -1;}
+
     default ArrayList<Long> getKeyFrameTimestampsUs() {
         return new ArrayList<>();
     }
@@ -135,4 +137,7 @@ class Chunk {
         public long timeUs;
         public boolean render;
     }
+    class KeyFrames {
+        public ArrayList<Long> keyFrameTimestampListUs;
+    }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index 18baf747..a2920390 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -15,6 +15,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -76,10 +77,6 @@ public void initialize() {
         for (int i = 0; i < mExtractor.getTrackCount(); i++) mExtractor.unselectTrack(i);
         mInitialized = true;
 
-        selectTrack(TrackType.VIDEO);
-        keyFrameTsUs.clear();
-        getSyncFrameTimestamps();
-        releaseTrack(TrackType.VIDEO);
         // Debugging mOriginUs issues.
         /* LOG.v("initialize(): origin after unselect is" + mExtractor.getSampleTime());
         if (getTrackFormat(TrackType.VIDEO) != null) {
@@ -99,22 +96,38 @@ public ArrayList<Long> getKeyFrameTimestampsUs() {
         return keyFrameTsUs;
     }
 
-    private void getSyncFrameTimestamps() {
-        long lastSampleTime = -1L;
+    @Override
+    public long requestKeyFrameTimestamps() {
+
+        int listSize = keyFrameTsUs.size();
+        if(listSize > 0) {
+            mExtractor.seekTo(keyFrameTsUs.get(listSize - 1) + 1, MediaExtractor.SEEK_TO_NEXT_SYNC);
+        }
+
         long sampleTime = mExtractor.getSampleTime();
 
+        if (sampleTime == -1) {
+            return sampleTime;
+        }
         LOG.i("start:" + sampleTime);
 
-        while (sampleTime >= 0L && sampleTime != lastSampleTime) {
+        int count = 0;
+        long lastSampleTime = -1L;
+        int prefetchCount = 100;
+        while (sampleTime >= 0L && sampleTime != lastSampleTime && count < prefetchCount) {
             if ((mExtractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) > 0) {
+                if (!keyFrameTsUs.isEmpty() && sampleTime <= keyFrameTsUs.get(listSize - 1)) {
+                    Collections.sort(keyFrameTsUs);
+                }
                 keyFrameTsUs.add(sampleTime);
             }
             mExtractor.seekTo(sampleTime + 1L, MediaExtractor.SEEK_TO_NEXT_SYNC);
             lastSampleTime = sampleTime;
             sampleTime = mExtractor.getSampleTime();
+            count++;
         }
-        mExtractor.seekTo(0, MediaExtractor.SEEK_TO_NEXT_SYNC);
         LOG.i("stop:" + keyFrameTsUs);
+        return sampleTime;
     }
 
 

From 18f9987d886aac684d54c2d4f40edae2be7f614a Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Thu, 20 Jan 2022 20:20:54 +0530
Subject: [PATCH 041/101] wip 2

---
 .../thumbnails/DefaultThumbnailsEngine.kt     | 74 +++++++++++++++----
 .../transcoder/source/DefaultDataSource.java  | 11 ++-
 .../thumbnail/SingleThumbnailRequest.kt       |  3 +-
 3 files changed, 67 insertions(+), 21 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index b25dd27e..c617ff05 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -20,15 +20,16 @@ import com.otaliastudios.transcoder.internal.video.VideoRenderer
 import com.otaliastudios.transcoder.internal.video.VideoSnapshots
 import com.otaliastudios.transcoder.resize.Resizer
 import com.otaliastudios.transcoder.source.DataSource
-import com.otaliastudios.transcoder.source.DefaultDataSource
 import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
 import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy
+import com.otaliastudios.transcoder.thumbnail.SingleThumbnailRequest
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
 import com.otaliastudios.transcoder.thumbnail.ThumbnailRequest
 import com.otaliastudios.transcoder.time.DefaultTimeInterpolator
 import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.isActive
+import java.util.ArrayList
 
 class DefaultThumbnailsEngine(
     private val dataSources: DataSources,
@@ -77,29 +78,54 @@ class DefaultThumbnailsEngine(
 
     private val stubs = ArrayDeque<Stub>()
 
+    private inner class IgnoringEosDataSource(
+        private val source: DataSource,
+    ) : DataSource by source {
+        override fun requestKeyFrameTimestamps(): Long {
+            return source.requestKeyFrameTimestamps()
+        }
+        override fun getKeyFrameTimestampsUs(): ArrayList<Long> {
+            return source.keyFrameTimestampsUs
+        }
+
+        override fun isDrained(): Boolean {
+            if(source.isDrained) {
+                source.seekTo(stubs.firstOrNull()?.positionUs ?: -1)
+            }
+            return source.isDrained
+        }
+    }
+    private fun DataSource.ignoringEOS(): DataSource = IgnoringEosDataSource(this)
+
     private fun createPipeline(
         type: TrackType,
         index: Int,
         status: TrackStatus,
         outputFormat: MediaFormat
     ): Pipeline {
-        val source = dataSources[type][index]
+        val source = dataSources[type][index].ignoringEOS()
+
         return Pipeline.build("Thumbnails") {
             Seeker(source) {
                 var seek = false
-                var seekUs = 0L
+                val seekUs: Long
                 val current = source.positionUs
                 val requested = stubs.firstOrNull()?.positionUs ?: -1
                 val threshold = stubs.firstOrNull()?.request?.threshold() ?: 0L
                 val nextKeyFrameIndex = source.search(requested)
 
                 val nextKeyFrameUs =
-                    if (nextKeyFrameIndex == -1) -1L else source.keyFrameTimestampsUs[nextKeyFrameIndex]
+                    if (nextKeyFrameIndex == -1) Long.MAX_VALUE else source.keyFrameTimestampsUs[nextKeyFrameIndex]
                 val previousKeyFrameUs =
-                    if (nextKeyFrameIndex > 0) source.keyFrameTimestampsUs[nextKeyFrameIndex - 1] else 0
+                    if (nextKeyFrameIndex > 0) {
+                        source.keyFrameTimestampsUs[nextKeyFrameIndex - 1]
+                    }
+                    else {
+                        source.keyFrameTimestampsUs[source.keyFrameTimestampsUs.size - 1]
+                    }
 
-                if (nextKeyFrameUs == -1L || !shouldSeek || requested == -1L) // no keyframe found, so can't seek.
-                    return@Seeker Pair(seekUs, seek)
+                if (!shouldSeek || requested == -1L)
+                    return@Seeker Pair(requested, seek)
 
                 log.i(
                     "seek: current ${source.positionUs}," +
@@ -145,20 +171,35 @@ class DefaultThumbnailsEngine(
 
     private lateinit var progress: (Thumbnail) -> Unit
 
-    private fun DataSource.search(timestampUs: Long) : Int  {
-        if(keyFrameTimestampsUs.isEmpty())
+    private fun DataSource.search(timestampUs: Long) : Int {
+        if (keyFrameTimestampsUs.isEmpty())
             requestKeyFrameTimestamps()
 
         val searchIndex = keyFrameTimestampsUs.binarySearch(timestampUs)
 
         val nextKeyFrameIndex = when {
-                searchIndex >= 0 -> searchIndex
-                searchIndex == -keyFrameTimestampsUs.size -> {
-                    if (requestKeyFrameTimestamps() == -1L) -1
-                    else search(timestampUs)
+            searchIndex >= 0 -> searchIndex
+            searchIndex < 0 -> {
+                val index = -searchIndex - 1
+                when {
+                    index >= keyFrameTimestampsUs.size -> {
+                        val ts = requestKeyFrameTimestamps()
+                        if (ts == -1L) {
+                            -1
+                        } else {
+                            search(timestampUs)
+                        }
+                    }
+                    index < keyFrameTimestampsUs.size -> index
+                    else -> {
+                        -1 // will never reach here. kotlin is stupid
+                    }
                 }
-                else -> { -searchIndex - 1 } // searchIndex < 0
             }
+            else -> {
+                -1 // will never reach here. kotlin is stupid
+            }
+        }
 
         return nextKeyFrameIndex
     }
@@ -190,7 +231,8 @@ class DefaultThumbnailsEngine(
         if (positionUs == stubs.firstOrNull()?.positionUs) {
             return
         }
-        val stub = stubs.find { it.positionUs == positionUs }
+        val locatedTimestampUs = SingleThumbnailRequest(positionUs).locate(timer.durationUs.video)[0]
+        val stub = stubs.find { it.positionUs == locatedTimestampUs }
         if (stub != null) {
             log.i("removePosition Match: $positionUs :$stubs")
             stubs.remove(stub)
@@ -209,7 +251,7 @@ class DefaultThumbnailsEngine(
             positions.mapNotNull { (positionUs, request) ->
                 val localizedUs = timer.localize(TrackType.VIDEO, index, positionUs)
                 localizedUs?.let { Stub(request, positionUs, localizedUs) }
-            }.toMutableList()
+            }.toMutableList().sortedBy { it.positionUs }
         )
     }
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index a2920390..91079d56 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -96,17 +96,20 @@ public ArrayList<Long> getKeyFrameTimestampsUs() {
         return keyFrameTsUs;
     }
 
+    private Boolean lastKeyFrame = false;
     @Override
     public long requestKeyFrameTimestamps() {
 
-        int listSize = keyFrameTsUs.size();
-        if(listSize > 0) {
-            mExtractor.seekTo(keyFrameTsUs.get(listSize - 1) + 1, MediaExtractor.SEEK_TO_NEXT_SYNC);
+        if(lastKeyFrame) return  -1L;
+        if(keyFrameTsUs.size() > 0) {
+            mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 1, MediaExtractor.SEEK_TO_NEXT_SYNC);
         }
 
         long sampleTime = mExtractor.getSampleTime();
 
         if (sampleTime == -1) {
+            lastKeyFrame = true;
+            mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 1, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
             return sampleTime;
         }
         LOG.i("start:" + sampleTime);
@@ -116,7 +119,7 @@ public long requestKeyFrameTimestamps() {
         int prefetchCount = 100;
         while (sampleTime >= 0L && sampleTime != lastSampleTime && count < prefetchCount) {
             if ((mExtractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) > 0) {
-                if (!keyFrameTsUs.isEmpty() && sampleTime <= keyFrameTsUs.get(listSize - 1)) {
+                if (!keyFrameTsUs.isEmpty() && sampleTime <= keyFrameTsUs.get(keyFrameTsUs.size() - 1)) {
                     Collections.sort(keyFrameTsUs);
                 }
                 keyFrameTsUs.add(sampleTime);
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
index 0411d6a3..9f4036af 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
@@ -2,7 +2,8 @@ package com.otaliastudios.transcoder.thumbnail
 
 open class SingleThumbnailRequest(private val positionUs: Long) : ThumbnailRequest {
     override fun locate(durationUs: Long): List<Long> {
-        val positionUs = positionUs.coerceIn(0L..durationUs)
+//        val positionUs = positionUs.coerceIn(0L..durationUs)
+        val positionUs = positionUs.coerceIn(0L..durationUs - 135005 - (positionUs / 1000) % 10000)
         return listOf(positionUs)
     }
 }

From 79efa783f055bcfc35844389535d03395abc7faf Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Fri, 21 Jan 2022 17:51:15 +0530
Subject: [PATCH 042/101] udpate documentation

---
 .../otaliastudios/transcoder/source/DefaultDataSource.java    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index 91079d56..c5a0ec1e 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -112,7 +112,7 @@ public long requestKeyFrameTimestamps() {
             mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 1, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
             return sampleTime;
         }
-        LOG.i("start:" + sampleTime);
+        LOG.i("keyFrameStartTime:" + sampleTime);
 
         int count = 0;
         long lastSampleTime = -1L;
@@ -129,7 +129,7 @@ public long requestKeyFrameTimestamps() {
             sampleTime = mExtractor.getSampleTime();
             count++;
         }
-        LOG.i("stop:" + keyFrameTsUs);
+        LOG.i("keyFrameStopCount:" + keyFrameTsUs);
         return sampleTime;
     }
 

From e28a858ce58313a0601ea1da242335259caf9abf Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Sat, 22 Jan 2022 02:00:17 +0530
Subject: [PATCH 043/101] rearrange code

---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt       | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index c617ff05..8428cd1d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -108,9 +108,13 @@ class DefaultThumbnailsEngine(
         return Pipeline.build("Thumbnails") {
             Seeker(source) {
                 var seek = false
+                val requested = stubs.firstOrNull()?.positionUs ?: -1
+
+                if (!shouldSeek || requested == -1L)
+                    return@Seeker Pair(requested, seek)
+
                 val seekUs: Long
                 val current = source.positionUs
-                val requested = stubs.firstOrNull()?.positionUs ?: -1
                 val threshold = stubs.firstOrNull()?.request?.threshold() ?: 0L
                 val nextKeyFrameIndex = source.search(requested)
 
@@ -124,9 +128,6 @@ class DefaultThumbnailsEngine(
                         source.keyFrameTimestampsUs[source.keyFrameTimestampsUs.size - 1]
                     }
 
-                if (!shouldSeek || requested == -1L)
-                    return@Seeker Pair(requested, seek)
-
                 log.i(
                     "seek: current ${source.positionUs}," +
                         " requested $requested, threshold $threshold, nextKeyFrameUs $nextKeyFrameUs"

From c74c41a10710f2b1534d9d8b96cb3b5836621121 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Mon, 24 Jan 2022 14:40:34 +0530
Subject: [PATCH 044/101] change seekToNext threshold to 1.01ms

---
 .../otaliastudios/transcoder/source/DefaultDataSource.java  | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index c5a0ec1e..68a88bdb 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -102,14 +102,14 @@ public long requestKeyFrameTimestamps() {
 
         if(lastKeyFrame) return  -1L;
         if(keyFrameTsUs.size() > 0) {
-            mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 1, MediaExtractor.SEEK_TO_NEXT_SYNC);
+            mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 1001L, MediaExtractor.SEEK_TO_NEXT_SYNC);
         }
 
         long sampleTime = mExtractor.getSampleTime();
 
         if (sampleTime == -1) {
             lastKeyFrame = true;
-            mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 1, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+            mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 1001L, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
             return sampleTime;
         }
         LOG.i("keyFrameStartTime:" + sampleTime);
@@ -124,7 +124,7 @@ public long requestKeyFrameTimestamps() {
                 }
                 keyFrameTsUs.add(sampleTime);
             }
-            mExtractor.seekTo(sampleTime + 1L, MediaExtractor.SEEK_TO_NEXT_SYNC);
+            mExtractor.seekTo(sampleTime + 1001L, MediaExtractor.SEEK_TO_NEXT_SYNC);
             lastSampleTime = sampleTime;
             sampleTime = mExtractor.getSampleTime();
             count++;

From d3563c317dbf6ffc005b9b999cef7d9948a3a99d Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Mon, 24 Jan 2022 17:10:31 +0530
Subject: [PATCH 045/101] change seekToNext threshold to 10.01ms

---
 .../otaliastudios/transcoder/source/DefaultDataSource.java  | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index 68a88bdb..85e90c04 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -102,14 +102,14 @@ public long requestKeyFrameTimestamps() {
 
         if(lastKeyFrame) return  -1L;
         if(keyFrameTsUs.size() > 0) {
-            mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 1001L, MediaExtractor.SEEK_TO_NEXT_SYNC);
+            mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 10001L, MediaExtractor.SEEK_TO_NEXT_SYNC);
         }
 
         long sampleTime = mExtractor.getSampleTime();
 
         if (sampleTime == -1) {
             lastKeyFrame = true;
-            mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 1001L, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+            mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 10001L, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
             return sampleTime;
         }
         LOG.i("keyFrameStartTime:" + sampleTime);
@@ -124,7 +124,7 @@ public long requestKeyFrameTimestamps() {
                 }
                 keyFrameTsUs.add(sampleTime);
             }
-            mExtractor.seekTo(sampleTime + 1001L, MediaExtractor.SEEK_TO_NEXT_SYNC);
+            mExtractor.seekTo(sampleTime + 10001L, MediaExtractor.SEEK_TO_NEXT_SYNC);
             lastSampleTime = sampleTime;
             sampleTime = mExtractor.getSampleTime();
             count++;

From 509030e8bd8f12a9c4968ad966e980a83566cb16 Mon Sep 17 00:00:00 2001
From: Dhruv <dhruv2295@gmail.com>
Date: Mon, 24 Jan 2022 22:59:16 +0530
Subject: [PATCH 046/101] ktlint and detekt formatting

---
 .../thumbnails/DefaultThumbnailsEngine.kt        | 16 +++++++---------
 .../thumbnail/SingleThumbnailRequest.kt          |  5 +++--
 2 files changed, 10 insertions(+), 11 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 8428cd1d..f71457d3 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -89,7 +89,7 @@ class DefaultThumbnailsEngine(
         }
 
         override fun isDrained(): Boolean {
-            if(source.isDrained) {
+            if (source.isDrained) {
                 source.seekTo(stubs.firstOrNull()?.positionUs ?: -1)
             }
             return source.isDrained
@@ -121,18 +121,16 @@ class DefaultThumbnailsEngine(
                 val nextKeyFrameUs =
                     if (nextKeyFrameIndex == -1) Long.MAX_VALUE else source.keyFrameTimestampsUs[nextKeyFrameIndex]
                 val previousKeyFrameUs =
-                    if (nextKeyFrameIndex > 0) {
-                        source.keyFrameTimestampsUs[nextKeyFrameIndex - 1]
-                    }
-                    else {
-                        source.keyFrameTimestampsUs[source.keyFrameTimestampsUs.size - 1]
-                    }
+                    source.keyFrameTimestampsUs[
+                        if (nextKeyFrameIndex > 0)
+                            nextKeyFrameIndex - 1 else (source.keyFrameTimestampsUs.size - 1)
+                    ]
 
                 log.i(
                     "seek: current ${source.positionUs}," +
                         " requested $requested, threshold $threshold, nextKeyFrameUs $nextKeyFrameUs"
                 )
-                
+
                 val rightGap = nextKeyFrameUs - requested
                 val nextKeyFrameInThreshold = rightGap <= threshold
                 seek = nextKeyFrameInThreshold || previousKeyFrameUs > current || (current - requested > threshold)
@@ -172,7 +170,7 @@ class DefaultThumbnailsEngine(
 
     private lateinit var progress: (Thumbnail) -> Unit
 
-    private fun DataSource.search(timestampUs: Long) : Int {
+    private fun DataSource.search(timestampUs: Long): Int {
         if (keyFrameTimestampsUs.isEmpty())
             requestKeyFrameTimestamps()
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
index 9f4036af..a40812ee 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt
@@ -1,9 +1,10 @@
 package com.otaliastudios.transcoder.thumbnail
 
+@Suppress("MagicNumber")
 open class SingleThumbnailRequest(private val positionUs: Long) : ThumbnailRequest {
     override fun locate(durationUs: Long): List<Long> {
-//        val positionUs = positionUs.coerceIn(0L..durationUs)
-        val positionUs = positionUs.coerceIn(0L..durationUs - 135005 - (positionUs / 1000) % 10000)
+        val randomizer = (positionUs / 1000) % 10000
+        val positionUs = positionUs.coerceIn(0L..durationUs - 135005 - randomizer)
         return listOf(positionUs)
     }
 }

From cd108395c8bb4b793e186c6ea1591e753ef79f3a Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Tue, 1 Feb 2022 00:37:16 +0530
Subject: [PATCH 047/101] add timeout to dequeoutputbuffer (#3)

---
 .../com/otaliastudios/transcoder/internal/codec/Decoder.kt     | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index 69f189fc..c59a42fb 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -41,6 +41,7 @@ class Decoder(
 
     companion object {
         private val ID = trackMapOf(AtomicInteger(0), AtomicInteger(0))
+        private const val timeoutUs = 2000L
     }
 
     private val log = Logger("Decoder(${format.trackType},${ID[format.trackType].getAndIncrement()})")
@@ -94,7 +95,7 @@ class Decoder(
     }
 
     override fun drain(): State<DecoderData> {
-        val result = codec.dequeueOutputBuffer(info, 0)
+        val result = codec.dequeueOutputBuffer(info, timeoutUs)
         return when (result) {
             INFO_TRY_AGAIN_LATER -> {
                 log.i("drain(): got INFO_TRY_AGAIN_LATER, waiting.")

From 93f3b1e4430cb31c83858ae7e5996744f9592343 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Tue, 22 Feb 2022 17:20:18 +0530
Subject: [PATCH 048/101] Transcode cleanup (#4)

* make progress listener nullable

* eof new line
---
 .../transcoder/TranscoderOptions.java         |  5 +-
 .../transcode/TranscodeDispatcher.java        | 58 -------------------
 .../internal/transcode/TranscodeDispatcher.kt | 28 +++++++++
 3 files changed, 29 insertions(+), 62 deletions(-)
 delete mode 100644 lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeDispatcher.java
 create mode 100644 lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeDispatcher.kt

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
index 61b35225..99343e8a 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
@@ -60,7 +60,6 @@ private TranscoderOptions() {}
     private TranscoderListener listener;
     private Handler listenerHandler;
 
-    @NonNull
     public TranscoderListener getListener() {
         return listener;
     }
@@ -341,9 +340,7 @@ public Builder setAudioResampler(@NonNull AudioResampler audioResampler) {
 
         @NonNull
         public TranscoderOptions build() {
-            if (listener == null) {
-                throw new IllegalStateException("listener can't be null");
-            }
+
             if (audioDataSources.isEmpty() && videoDataSources.isEmpty()) {
                 throw new IllegalStateException("we need at least one data source");
             }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeDispatcher.java b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeDispatcher.java
deleted file mode 100644
index 2db927ed..00000000
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeDispatcher.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.otaliastudios.transcoder.internal.transcode;
-
-import android.os.Handler;
-
-import androidx.annotation.NonNull;
-
-import com.otaliastudios.transcoder.TranscoderListener;
-import com.otaliastudios.transcoder.TranscoderOptions;
-
-/**
- * Wraps a TranscoderListener and posts events on the given handler.
- */
-class TranscodeDispatcher {
-
-    private final Handler mHandler;
-    private final TranscoderListener mListener;
-
-    TranscodeDispatcher(@NonNull TranscoderOptions options) {
-        mHandler = options.getListenerHandler();
-        mListener = options.getListener();
-    }
-
-    void dispatchCancel() {
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                mListener.onTranscodeCanceled();
-            }
-        });
-    }
-
-    void dispatchSuccess(final int successCode) {
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                mListener.onTranscodeCompleted(successCode);
-            }
-        });
-    }
-
-    void dispatchFailure(@NonNull final Throwable exception) {
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                mListener.onTranscodeFailed(exception);
-            }
-        });
-    }
-
-    void dispatchProgress(final double progress) {
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                mListener.onTranscodeProgress(progress);
-            }
-        });
-    }
-}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeDispatcher.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeDispatcher.kt
new file mode 100644
index 00000000..37f6e5c0
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeDispatcher.kt
@@ -0,0 +1,28 @@
+package com.otaliastudios.transcoder.internal.transcode
+
+import com.otaliastudios.transcoder.TranscoderOptions
+import com.otaliastudios.transcoder.TranscoderListener
+
+/**
+ * Wraps a TranscoderListener and posts events on the given handler.
+ */
+internal class TranscodeDispatcher(options: TranscoderOptions) {
+    private val mHandler = options.listenerHandler
+    private val mListener = options.listener
+    fun dispatchCancel() {
+        mHandler.post { mListener?.onTranscodeCanceled() }
+    }
+
+    fun dispatchSuccess(successCode: Int) {
+        mHandler.post { mListener?.onTranscodeCompleted(successCode) }
+    }
+
+    fun dispatchFailure(exception: Throwable) {
+        mHandler.post { mListener?.onTranscodeFailed(exception) }
+    }
+
+    fun dispatchProgress(progress: Double) {
+        mHandler.post { mListener?.onTranscodeProgress(progress) }
+    }
+
+}

From 671ddd00821de3e59c8e5389da6a37e9773fd14a Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Mon, 28 Feb 2022 22:39:41 +0530
Subject: [PATCH 049/101] rename list variable (#6)

* rename list variable

* Fix data source keyframe list

Co-authored-by: Sahil Bajaj <sahilbajaj.nc@gmail.com>
---
 .../thumbnails/DefaultThumbnailsEngine.kt     | 29 ++++++++++---------
 .../transcoder/source/DataSource.java         |  3 +-
 .../transcoder/source/DefaultDataSource.java  | 22 +++++++-------
 3 files changed, 29 insertions(+), 25 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index f71457d3..123748ac 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -84,8 +84,8 @@ class DefaultThumbnailsEngine(
         override fun requestKeyFrameTimestamps(): Long {
             return source.requestKeyFrameTimestamps()
         }
-        override fun getKeyFrameTimestampsUs(): ArrayList<Long> {
-            return source.keyFrameTimestampsUs
+        override fun getKeyFrameTimestamps(): ArrayList<Long> {
+            return source.keyFrameTimestamps
         }
 
         override fun isDrained(): Boolean {
@@ -118,13 +118,8 @@ class DefaultThumbnailsEngine(
                 val threshold = stubs.firstOrNull()?.request?.threshold() ?: 0L
                 val nextKeyFrameIndex = source.search(requested)
 
-                val nextKeyFrameUs =
-                    if (nextKeyFrameIndex == -1) Long.MAX_VALUE else source.keyFrameTimestampsUs[nextKeyFrameIndex]
-                val previousKeyFrameUs =
-                    source.keyFrameTimestampsUs[
-                        if (nextKeyFrameIndex > 0)
-                            nextKeyFrameIndex - 1 else (source.keyFrameTimestampsUs.size - 1)
-                    ]
+                val nextKeyFrameUs = source.keyFrameAt(nextKeyFrameIndex) { Long.MAX_VALUE }
+                val previousKeyFrameUs = source.keyFrameAt(nextKeyFrameIndex - 1) { source.lastKeyFrame() }
 
                 log.i(
                     "seek: current ${source.positionUs}," +
@@ -170,18 +165,26 @@ class DefaultThumbnailsEngine(
 
     private lateinit var progress: (Thumbnail) -> Unit
 
+    private fun DataSource.lastKeyFrame(): Long {
+        return keyFrameAt(keyFrameTimestamps.size - 1)
+    }
+
+    private inline fun DataSource.keyFrameAt(index: Int, defaultValue: ((Int)-> Long) = {_ -> -1}): Long {
+        return keyFrameTimestamps.getOrElse(index, defaultValue)
+    }
+
     private fun DataSource.search(timestampUs: Long): Int {
-        if (keyFrameTimestampsUs.isEmpty())
+        if (keyFrameTimestamps.isEmpty())
             requestKeyFrameTimestamps()
 
-        val searchIndex = keyFrameTimestampsUs.binarySearch(timestampUs)
+        val searchIndex = keyFrameTimestamps.binarySearch(timestampUs)
 
         val nextKeyFrameIndex = when {
             searchIndex >= 0 -> searchIndex
             searchIndex < 0 -> {
                 val index = -searchIndex - 1
                 when {
-                    index >= keyFrameTimestampsUs.size -> {
+                    index >= keyFrameTimestamps.size -> {
                         val ts = requestKeyFrameTimestamps()
                         if (ts == -1L) {
                             -1
@@ -189,7 +192,7 @@ class DefaultThumbnailsEngine(
                             search(timestampUs)
                         }
                     }
-                    index < keyFrameTimestampsUs.size -> index
+                    index < keyFrameTimestamps.size -> index
                     else -> {
                         -1 // will never reach here. kotlin is stupid
                     }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
index c6e2efe7..ab469fec 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
@@ -114,9 +114,10 @@ public interface DataSource {
 
     default long requestKeyFrameTimestamps() { return -1;}
 
-    default ArrayList<Long> getKeyFrameTimestampsUs() {
+    default ArrayList<Long> getKeyFrameTimestamps() {
         return new ArrayList<>();
     }
+
     /**
      * Rewinds this source, moving it to its default state.
      * To be used again, tracks will be selected again.
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index 3faf7422..7e7c1beb 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -45,7 +45,7 @@ public abstract class DefaultDataSource implements DataSource {
     private long mDontRenderRangeStart = -1L;
     private long mDontRenderRangeEnd = -1L;
 
-    private final ArrayList<Long> keyFrameTsUs = new ArrayList<>();
+    private final ArrayList<Long> keyFrameTimestamps = new ArrayList<>();
     @Override
     public void initialize() {
         LOG.i("initialize(): initializing...");
@@ -92,8 +92,8 @@ public void initialize() {
     }
 
     @Override
-    public ArrayList<Long> getKeyFrameTimestampsUs() {
-        return keyFrameTsUs;
+    public ArrayList<Long> getKeyFrameTimestamps() {
+        return keyFrameTimestamps;
     }
 
     private Boolean lastKeyFrame = false;
@@ -101,15 +101,15 @@ public ArrayList<Long> getKeyFrameTimestampsUs() {
     public long requestKeyFrameTimestamps() {
 
         if(lastKeyFrame) return  -1L;
-        if(keyFrameTsUs.size() > 0) {
-            mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 10001L, MediaExtractor.SEEK_TO_NEXT_SYNC);
+        if(keyFrameTimestamps.size() > 0) {
+            mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + 10001L, MediaExtractor.SEEK_TO_NEXT_SYNC);
         }
 
         long sampleTime = mExtractor.getSampleTime();
 
-        if (sampleTime == -1 || (keyFrameTsUs.size() > 0 && sampleTime == keyFrameTsUs.get(keyFrameTsUs.size() - 1))) {
+        if (sampleTime == -1 || (keyFrameTimestamps.size() > 0 && sampleTime == keyFrameTimestamps.get(keyFrameTimestamps.size() - 1))) {
             lastKeyFrame = true;
-            mExtractor.seekTo(keyFrameTsUs.get(keyFrameTsUs.size() - 1) + 10001L, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+            mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + 10001L, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
             return -1;
         }
         LOG.i("keyFrameStartTime:" + sampleTime);
@@ -119,17 +119,17 @@ public long requestKeyFrameTimestamps() {
         int prefetchCount = 100;
         while (sampleTime >= 0L && sampleTime != lastSampleTime && count < prefetchCount) {
             if ((mExtractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) > 0) {
-                if (!keyFrameTsUs.isEmpty() && sampleTime <= keyFrameTsUs.get(keyFrameTsUs.size() - 1)) {
-                    Collections.sort(keyFrameTsUs);
+                if (!keyFrameTimestamps.isEmpty() && sampleTime <= keyFrameTimestamps.get(keyFrameTimestamps.size() - 1)) {
+                    Collections.sort(keyFrameTimestamps);
                 }
-                keyFrameTsUs.add(sampleTime);
+                keyFrameTimestamps.add(sampleTime);
             }
             mExtractor.seekTo(sampleTime + 10001L, MediaExtractor.SEEK_TO_NEXT_SYNC);
             lastSampleTime = sampleTime;
             sampleTime = mExtractor.getSampleTime();
             count++;
         }
-        LOG.i("keyFrameStopCount:" + keyFrameTsUs);
+        LOG.i("keyFrameStopCount:" + keyFrameTimestamps);
         return sampleTime;
     }
 

From c28262988e24f49b682c6008a97fc2c423c18343 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Mon, 28 Feb 2022 22:45:51 +0530
Subject: [PATCH 050/101] create sort buckets for improving thumbnail
 performance (#5)

Create sort buckets for improving thumbnail performance

Co-authored-by: Sahil Bajaj <sahilbajaj.nc@gmail.com>
---
 .../thumbnails/DefaultThumbnailsEngine.kt     | 20 ++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 123748ac..e2b63eb4 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -253,10 +253,28 @@ class DefaultThumbnailsEngine(
             positions.mapNotNull { (positionUs, request) ->
                 val localizedUs = timer.localize(TrackType.VIDEO, index, positionUs)
                 localizedUs?.let { Stub(request, positionUs, localizedUs) }
-            }.toMutableList().sortedBy { it.positionUs }
+//            }.toMutableList().sortedBy { it.positionUs }
+            }.toMutableList().reorder(dataSources[TrackType.VIDEO][0])
         )
     }
 
+    private fun  List<Stub>.reorder(source: DataSource): Collection<Stub> {
+        val bucketListMap = LinkedHashMap<Long, ArrayList<Stub>>()
+        val finalList = ArrayList<Stub>()
+
+        forEach {
+            val nextKeyFrameIndex = source.search(it.positionUs)
+            val previousKeyFrameUs = source.keyFrameAt(nextKeyFrameIndex - 1) { source.lastKeyFrame() }
+
+            val list = bucketListMap.getOrPut(previousKeyFrameUs) { ArrayList<Stub>() }
+            list.add(it)
+        }
+        bucketListMap.forEach {
+            finalList.addAll(it.value.sortedBy { it.positionUs })
+        }
+        return finalList
+    }
+
     private val fetchPosition: () -> VideoSnapshots.Request? = {
         if (stubs.isEmpty()) null
         else VideoSnapshots.Request(stubs.first().localizedUs, stubs.first().request.threshold())

From 952510884665aea8875960d1e8c7489c5a9eb5c7 Mon Sep 17 00:00:00 2001
From: soywiz-invideo <99644907+soywiz-invideo@users.noreply.github.com>
Date: Thu, 21 Apr 2022 21:12:38 +0200
Subject: [PATCH 051/101] Circumvents gradle leak (#8)

---
 lib/build.gradle.kts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index 72f8b017..5fef8d19 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -6,7 +6,7 @@ import io.deepmedia.tools.publisher.sonatype.Sonatype
 plugins {
     id("com.android.library")
     id("kotlin-android")
-    id("io.deepmedia.tools.publisher")
+//    id("io.deepmedia.tools.publisher")
 }
 
 android {
@@ -32,6 +32,7 @@ dependencies {
     androidTestImplementation("org.mockito:mockito-android:2.28.2")
 }
 
+/*
 publisher {
     project.description = "Accelerated video transcoding using Android MediaCodec API without native code (no LGPL/patent issues)."
     project.artifact = "transcoder"
@@ -61,3 +62,4 @@ publisher {
         signing.password = "SIGNING_PASSWORD"
     }
 }
+*/

From ec0021a88ee61b9d24252b63a615c03a3e7ed7f6 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Sun, 8 May 2022 00:53:33 +0530
Subject: [PATCH 052/101] seek threshold (#11)

* expose seek threshold in data source to overcome extractor shortcomings and seek accurately
---
 .../transcoder/internal/codec/Decoder.kt      | 10 ++++++
 .../thumbnails/DefaultThumbnailsEngine.kt     | 18 +++++++---
 .../transcoder/source/DataSource.java         |  4 +++
 .../transcoder/source/DefaultDataSource.java  | 35 ++++++++++++++-----
 4 files changed, 53 insertions(+), 14 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index c59a42fb..478dd3f5 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -42,6 +42,7 @@ class Decoder(
     companion object {
         private val ID = trackMapOf(AtomicInteger(0), AtomicInteger(0))
         private const val timeoutUs = 2000L
+        private const val VERBOSE = false
     }
 
     private val log = Logger("Decoder(${format.trackType},${ID[format.trackType].getAndIncrement()})")
@@ -90,6 +91,9 @@ class Decoder(
         dequeuedInputs--
         val (chunk, id) = data
         val flag = if (chunk.keyframe) BUFFER_FLAG_SYNC_FRAME else 0
+        if(VERBOSE) {
+            log.v("enqueue(): queueInputBuffer ${chunk.timeUs}")
+        }
         codec.queueInputBuffer(id, chunk.buffer.position(), chunk.buffer.remaining(), chunk.timeUs, flag)
         dropper.input(chunk.timeUs, chunk.render)
     }
@@ -123,11 +127,17 @@ class Decoder(
                     // Ideally, we shouldn't rely on the fact that the buffer is properly configured.
                     // We should configure its position and limit based on the buffer info's position and size.
                     val data = DecoderData(buffer, timeUs) {
+                        if(VERBOSE) {
+                            log.v("drain(): released successfully presentation ts ${info.presentationTimeUs} and $timeUs")
+                        }
                         codec.releaseOutputBuffer(result, it)
                         dequeuedOutputs--
                     }
                     if (isEos) State.Eos(data) else State.Ok(data)
                 } else {
+                    if(VERBOSE) {
+                        log.v("drain(): released because decoder dropper gave null ts ${info.presentationTimeUs}")
+                    }
                     codec.releaseOutputBuffer(result, false)
                     State.Wait
                 }.also {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index e2b63eb4..9dda8495 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -88,6 +88,8 @@ class DefaultThumbnailsEngine(
             return source.keyFrameTimestamps
         }
 
+        override fun getSeekThreshold() = source.seekThreshold
+
         override fun isDrained(): Boolean {
             if (source.isDrained) {
                 source.seekTo(stubs.firstOrNull()?.positionUs ?: -1)
@@ -121,15 +123,20 @@ class DefaultThumbnailsEngine(
                 val nextKeyFrameUs = source.keyFrameAt(nextKeyFrameIndex) { Long.MAX_VALUE }
                 val previousKeyFrameUs = source.keyFrameAt(nextKeyFrameIndex - 1) { source.lastKeyFrame() }
 
-                log.i(
-                    "seek: current ${source.positionUs}," +
-                        " requested $requested, threshold $threshold, nextKeyFrameUs $nextKeyFrameUs"
-                )
 
                 val rightGap = nextKeyFrameUs - requested
                 val nextKeyFrameInThreshold = rightGap <= threshold
                 seek = nextKeyFrameInThreshold || previousKeyFrameUs > current || (current - requested > threshold)
-                seekUs = if (nextKeyFrameInThreshold) nextKeyFrameUs else previousKeyFrameUs
+                seekUs =
+                    (if (nextKeyFrameInThreshold) nextKeyFrameUs else previousKeyFrameUs) + source.seekThreshold
+
+                if (VERBOSE) {
+                    log.i(
+                        "seek: current ${source.positionUs}," +
+                                " requested $requested, threshold $threshold, nextKeyFrameUs $nextKeyFrameUs," +
+                                " nextKeyFrameInThreshold:$nextKeyFrameInThreshold, seekUs: $seekUs, flushing : $seek"
+                    )
+                }
 
                 shouldFlush = seek
                 shouldSeek = false
@@ -287,5 +294,6 @@ class DefaultThumbnailsEngine(
 
     companion object {
         private const val WAIT_MS = 5L
+        private const val VERBOSE = false
     }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
index ab469fec..030756fa 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
@@ -118,6 +118,10 @@ default ArrayList<Long> getKeyFrameTimestamps() {
         return new ArrayList<>();
     }
 
+    default long getSeekThreshold() {
+        return 0;
+    }
+    default String mediaId() { return "";}
     /**
      * Rewinds this source, moving it to its default state.
      * To be used again, tracks will be selected again.
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index 7e7c1beb..7eddb98d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -46,6 +46,8 @@ public abstract class DefaultDataSource implements DataSource {
     private long mDontRenderRangeEnd = -1L;
 
     private final ArrayList<Long> keyFrameTimestamps = new ArrayList<>();
+    private final long SEEK_THRESHOLD = 10001L; // 10ms because extractor doesn't seek accurately
+    private final boolean VERBOSE = false;
     @Override
     public void initialize() {
         LOG.i("initialize(): initializing...");
@@ -102,14 +104,14 @@ public long requestKeyFrameTimestamps() {
 
         if(lastKeyFrame) return  -1L;
         if(keyFrameTimestamps.size() > 0) {
-            mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + 10001L, MediaExtractor.SEEK_TO_NEXT_SYNC);
+            mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + SEEK_THRESHOLD, MediaExtractor.SEEK_TO_NEXT_SYNC);
         }
 
         long sampleTime = mExtractor.getSampleTime();
 
         if (sampleTime == -1 || (keyFrameTimestamps.size() > 0 && sampleTime == keyFrameTimestamps.get(keyFrameTimestamps.size() - 1))) {
             lastKeyFrame = true;
-            mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + 10001L, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+            mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + SEEK_THRESHOLD, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
             return -1;
         }
         LOG.i("keyFrameStartTime:" + sampleTime);
@@ -124,7 +126,7 @@ public long requestKeyFrameTimestamps() {
                 }
                 keyFrameTimestamps.add(sampleTime);
             }
-            mExtractor.seekTo(sampleTime + 10001L, MediaExtractor.SEEK_TO_NEXT_SYNC);
+            mExtractor.seekTo(sampleTime + SEEK_THRESHOLD, MediaExtractor.SEEK_TO_NEXT_SYNC);
             lastSampleTime = sampleTime;
             sampleTime = mExtractor.getSampleTime();
             count++;
@@ -134,6 +136,11 @@ public long requestKeyFrameTimestamps() {
     }
 
 
+    @Override
+    public long getSeekThreshold() {
+        return SEEK_THRESHOLD;
+    }
+
     @Override
     public void deinitialize() {
         LOG.i("deinitialize(): deinitializing...");
@@ -189,13 +196,15 @@ public void releaseTrack(@NonNull TrackType type) {
     public long seekTo(long desiredPositionUs) {
         boolean hasVideo = mSelectedTracks.contains(TrackType.VIDEO);
         boolean hasAudio = mSelectedTracks.contains(TrackType.AUDIO);
-        LOG.i("seekTo(): seeking to " + (mOriginUs + desiredPositionUs)
-                + " originUs=" + mOriginUs
-                + " extractorUs=" + mExtractor.getSampleTime()
-                + " externalUs=" + desiredPositionUs
-                + " hasVideo=" + hasVideo
-                + " hasAudio=" + hasAudio);
         if (hasVideo && hasAudio) {
+            if (VERBOSE) {
+                LOG.i("seekTo(): seeking to " + (mOriginUs + desiredPositionUs)
+                        + " originUs=" + mOriginUs
+                        + " extractorUs=" + mExtractor.getSampleTime()
+                        + " externalUs=" + desiredPositionUs
+                        + " hasVideo=" + hasVideo
+                        + " hasAudio=" + hasAudio);
+            }
             // Special case: audio can be moved to any timestamp, but video will only stop in
             // sync frames. MediaExtractor is not smart enough to sync the two tracks at the
             // video sync frame, so we must take care of this with the following trick.
@@ -208,6 +217,14 @@ public long seekTo(long desiredPositionUs) {
             mExtractor.seekTo(mExtractor.getSampleTime(), MediaExtractor.SEEK_TO_CLOSEST_SYNC);
             LOG.v("seekTo(): seek workaround completed. (extractorUs=" + mExtractor.getSampleTime() + ")");
         } else {
+            if (VERBOSE) {
+                LOG.i("seekTo(): seeking to " + (desiredPositionUs)
+                        + " originUs=" + mOriginUs
+                        + " extractorUs=" + mExtractor.getSampleTime()
+                        + " externalUs=" + desiredPositionUs
+                        + " hasVideo=" + hasVideo
+                        + " hasAudio=" + hasAudio);
+            }
             mExtractor.seekTo(desiredPositionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
         }
         mDontRenderRangeStart = mExtractor.getSampleTime();

From 0bd87a0a9f535d9880383b459318d98004115508 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Sun, 8 May 2022 00:55:46 +0530
Subject: [PATCH 053/101] flow based callback for thumbnail generation (#12)

---
 .../thumbnails/DefaultThumbnailsEngine.kt     | 33 ++++++++++---------
 .../internal/thumbnails/ThumbnailsEngine.kt   | 24 +++-----------
 2 files changed, 21 insertions(+), 36 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 9dda8495..fefdaaf6 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -26,8 +26,11 @@ import com.otaliastudios.transcoder.thumbnail.SingleThumbnailRequest
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
 import com.otaliastudios.transcoder.thumbnail.ThumbnailRequest
 import com.otaliastudios.transcoder.time.DefaultTimeInterpolator
+import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.isActive
 import java.util.ArrayList
 
@@ -81,12 +84,10 @@ class DefaultThumbnailsEngine(
     private inner class IgnoringEosDataSource(
         private val source: DataSource,
     ) : DataSource by source {
-        override fun requestKeyFrameTimestamps(): Long {
-            return source.requestKeyFrameTimestamps()
-        }
-        override fun getKeyFrameTimestamps(): ArrayList<Long> {
-            return source.keyFrameTimestamps
-        }
+
+        override fun requestKeyFrameTimestamps() = source.requestKeyFrameTimestamps()
+
+        override fun getKeyFrameTimestamps() = source.keyFrameTimestamps
 
         override fun getSeekThreshold() = source.seekThreshold
 
@@ -164,21 +165,22 @@ class DefaultThumbnailsEngine(
                                 "deltaUs=${stub.localizedUs - stub.actualLocalizedUs}"
                         )
                         val thumbnail = Thumbnail(stub.request, stub.positionUs, bitmap)
-                        progress(thumbnail)
+                        val callbackStatus = progress.trySend(thumbnail)
+                        log.i("Callback Send Status ${callbackStatus.isSuccess}")
                     }
                 }
         }
     }
 
-    private lateinit var progress: (Thumbnail) -> Unit
+    private val progress = Channel<Thumbnail>(Channel.BUFFERED)
 
-    private fun DataSource.lastKeyFrame(): Long {
-        return keyFrameAt(keyFrameTimestamps.size - 1)
-    }
 
-    private inline fun DataSource.keyFrameAt(index: Int, defaultValue: ((Int)-> Long) = {_ -> -1}): Long {
-        return keyFrameTimestamps.getOrElse(index, defaultValue)
-    }
+    private fun DataSource.lastKeyFrame() = keyFrameAt(keyFrameTimestamps.size - 1)
+
+    override val progressFlow: Flow<Thumbnail> = progress.receiveAsFlow()
+
+    private inline fun DataSource.keyFrameAt(index: Int, defaultValue: ((Int)-> Long) = {_ -> -1}) =
+        keyFrameTimestamps.getOrElse(index, defaultValue)
 
     private fun DataSource.search(timestampUs: Long): Int {
         if (keyFrameTimestamps.isEmpty())
@@ -213,12 +215,11 @@ class DefaultThumbnailsEngine(
         return nextKeyFrameIndex
     }
 
-    override suspend fun queueThumbnails(list: List<ThumbnailRequest>, progress: (Thumbnail) -> Unit) {
+    override suspend fun queueThumbnails(list: List<ThumbnailRequest>) {
         val segment = segments.next(TrackType.VIDEO)
         segment?.let {
             this.updatePositions(list, it.index)
         }
-        this.progress = progress
         while (currentCoroutineContext().isActive) {
             val advanced = segments.next(TrackType.VIDEO)?.advance() ?: false
             val completed = !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index 89aa8f47..8d4c5c3d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -7,10 +7,13 @@ import com.otaliastudios.transcoder.internal.DataSources
 import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
 import com.otaliastudios.transcoder.thumbnail.ThumbnailRequest
+import kotlinx.coroutines.flow.Flow
 
 abstract class ThumbnailsEngine {
 
-    abstract suspend fun queueThumbnails(list: List<ThumbnailRequest>, progress: (Thumbnail) -> Unit)
+    abstract val progressFlow: Flow<Thumbnail>
+
+    abstract suspend fun queueThumbnails(list: List<ThumbnailRequest>)
 
     abstract suspend fun removePosition(positionUs: Long)
 
@@ -43,24 +46,5 @@ abstract class ThumbnailsEngine {
             return engine
         }
     }
-    suspend fun queue(list: List<ThumbnailRequest>) {
-        engine?.queueThumbnails(list) {
-            dispatcher.dispatchThumbnail(it)
-        }
 
-        try {
-            dispatcher.dispatchCompletion()
-        } catch (e: Exception) {
-            if (e.isInterrupted()) {
-                log.i("Transcode canceled.", e)
-                dispatcher.dispatchCancel()
-            } else {
-                log.e("Unexpected error while transcoding.", e)
-                dispatcher.dispatchFailure(e)
-                throw e
-            }
-        } finally {
-            engine?.cleanup()
-        }
-    }
 }

From 6cd855ba2d638516038c18102bada91b547678fe Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Sun, 8 May 2022 00:59:50 +0530
Subject: [PATCH 054/101] Bug fix/single instance dependent checks removal
 (#13)

* thumbnailer can have 0 data sources now. and rotation can be different.
---
 .../com/otaliastudios/transcoder/ThumbnailerOptions.kt |  6 +++---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt     |  4 +++-
 .../transcoder/internal/video/VideoRenderer.kt         | 10 +++++-----
 3 files changed, 11 insertions(+), 9 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
index 2d74a303..713a5910 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
@@ -84,9 +84,9 @@ class ThumbnailerOptions(
         }
 
         fun build(): ThumbnailerOptions {
-            require(dataSources.isNotEmpty()) {
-                "At least one data source is required!"
-            }
+//            require(dataSources.isNotEmpty()) {
+//                "At least one data source is required!"
+//            }
 
             val listenerHandler = listenerHandler
                 ?: Handler(Looper.myLooper() ?: Looper.getMainLooper())
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index fefdaaf6..bead87b0 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -166,7 +166,9 @@ class DefaultThumbnailsEngine(
                         )
                         val thumbnail = Thumbnail(stub.request, stub.positionUs, bitmap)
                         val callbackStatus = progress.trySend(thumbnail)
-                        log.i("Callback Send Status ${callbackStatus.isSuccess}")
+                        if (VERBOSE) {
+                            log.i("Callback Send Status ${callbackStatus.isSuccess}")
+                        }
                     }
                 }
         }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
index 06dbaf2c..e79e1805 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoRenderer.kt
@@ -57,10 +57,10 @@ class VideoRenderer(
 
         // Just a sanity check that the rotation coming from DataSource is not different from
         // the one found in the DataSource's MediaFormat for video.
-        val sourceRotation = runCatching { sourceFormat.getInteger(KEY_ROTATION_DEGREES) }.getOrElse { 0 }
-        if (sourceRotation != this.sourceRotation) {
-            error("Unexpected difference in rotation. DataSource=${this.sourceRotation}, MediaFormat=$sourceRotation")
-        }
+//        val sourceRotation = runCatching { sourceFormat.getInteger(KEY_ROTATION_DEGREES) }.getOrElse { 0 }
+//        if (sourceRotation != this.sourceRotation) {
+//            error("Unexpected difference in rotation. DataSource=${this.sourceRotation}, MediaFormat=$sourceRotation")
+//        }
 
         // Decoded video is rotated automatically starting from Android 5.0. Turn it off here because we
         // don't want to work on the rotated one, we apply rotation at rendering time.
@@ -68,7 +68,7 @@ class VideoRenderer(
         sourceFormat.setInteger(KEY_ROTATION_DEGREES, 0)
 
         // Instead, apply the source rotation, plus the extra user rotation, to the renderer.
-        val rotation = (sourceRotation + extraRotation) % 360
+        val rotation = (this.sourceRotation + extraRotation) % 360
         frameDrawer.setRotation(rotation)
 
         // Depending on the rotation, we must also pass scale to the drawer due to how GL works.

From ca1916a4083cd2819faa8ca339b084549ffcebb7 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Sun, 8 May 2022 01:04:15 +0530
Subject: [PATCH 055/101] expose media id in datasource (#14)

---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt           | 4 +++-
 .../com/otaliastudios/transcoder/source/UriDataSource.java   | 5 +++++
 .../otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt   | 2 ++
 3 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index bead87b0..c2ae7066 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -75,7 +75,7 @@ class DefaultThumbnailsEngine(
     ) {
         var actualLocalizedUs: Long = localizedUs
         override fun toString(): String {
-            return positionUs.toString()
+            return request.sourceId() + ":" + positionUs.toString()
         }
     }
 
@@ -91,6 +91,8 @@ class DefaultThumbnailsEngine(
 
         override fun getSeekThreshold() = source.seekThreshold
 
+        override fun mediaId() = source.mediaId()
+
         override fun isDrained(): Boolean {
             if (source.isDrained) {
                 source.seekTo(stubs.firstOrNull()?.positionUs ?: -1)
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java
index 82562773..65c8fc7d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java
@@ -32,4 +32,9 @@ protected void initializeExtractor(@NonNull MediaExtractor extractor) throws IOE
     protected void initializeRetriever(@NonNull MediaMetadataRetriever retriever) {
         retriever.setDataSource(context, uri);
     }
+
+    @Override
+    public String mediaId() {
+        return uri.toString();
+    }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt
index 8724b7ba..89a669ff 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt
@@ -4,6 +4,8 @@ interface ThumbnailRequest {
     fun locate(durationUs: Long): List<Long>
 
     fun threshold(): Long = 0
+
+    fun sourceId() : String = ""
     // Could make it so that if locate() is empty, accept is called for each frame (no seeking).
     // But this only makes sense if accept signature has more information (segment, ...), and
     // it should also have a way to say - we're done, stop transcoding.

From afa797c4fc19e5531c99a46938e643c4b9bb7eaa Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Sun, 8 May 2022 02:09:43 +0530
Subject: [PATCH 056/101] single thumbnailer instance (#9)

* single instance support setup
* destroy segment logic improved
---
 .../transcoder/internal/CustomSegments.kt     | 96 +++++++++++++++++++
 .../transcoder/internal/DataSources.kt        | 74 +++++++++++---
 .../transcoder/internal/Tracks.kt             | 27 +++---
 .../thumbnails/DefaultThumbnailsEngine.kt     | 90 ++++++++++-------
 .../internal/thumbnails/ThumbnailsEngine.kt   |  7 +-
 .../transcoder/thumbnail/Thumbnail.kt         |  2 +-
 6 files changed, 238 insertions(+), 58 deletions(-)
 create mode 100644 lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
new file mode 100644
index 00000000..e2546335
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
@@ -0,0 +1,96 @@
+@file:Suppress("ReturnCount")
+
+package com.otaliastudios.transcoder.internal
+
+import android.media.MediaFormat
+import com.otaliastudios.transcoder.common.TrackStatus
+import com.otaliastudios.transcoder.common.TrackType
+import com.otaliastudios.transcoder.internal.pipeline.Pipeline
+import com.otaliastudios.transcoder.internal.utils.Logger
+import com.otaliastudios.transcoder.internal.utils.mutableTrackMapOf
+import com.otaliastudios.transcoder.source.DataSource
+
+class CustomSegments(
+    private val sources: DataSources,
+    private val tracks: Tracks,
+    private val factory: (TrackType, Int, TrackStatus, MediaFormat) -> Pipeline,
+) {
+
+    private val log = Logger("Segments")
+    private var currentSegment: Segment? = null
+    val currentIndex = mutableTrackMapOf(-1, -1)
+    private val segmentMap = mutableMapOf<String, Segment?>()
+
+    fun hasNext(type: TrackType): Boolean {
+        if (!sources.has(type)) return false
+        log.v(
+            "hasNext($type): segment=${currentSegment} lastIndex=${sources.getOrNull(type)?.lastIndex}" +
+                    " canAdvance=${currentSegment?.canAdvance()}"
+        )
+        val segment = currentSegment ?: return true // not started
+        val lastIndex = sources.getOrNull(type)?.lastIndex ?: return false // no track!
+        return segment.canAdvance() || segment.index < lastIndex
+    }
+
+    fun hasNext() = hasNext(TrackType.VIDEO)
+
+
+    // it will be time dependent
+    // 1. make segments work for thumbnails as is
+    // 2. inject segments dynamically
+    // 3. seek to segment and destroy previous ones
+    // 4. destroy only if necessary, else reuse
+
+    fun getSegment(id: String): Segment? {
+        return segmentMap.getOrPut(id) {
+            destroySegment(id)
+            tryCreateSegment(id).also {
+                currentSegment = it
+            }
+        }
+    }
+
+    fun release() = destroySegment()
+
+    private fun tryCreateSegment(id: String): Segment? {
+        val index = sources[TrackType.VIDEO].indexOfFirst { it.mediaId() == id }
+        // Return null if out of bounds, either because segments are over or because the
+        // source set does not have sources for this track type.
+        val source = sources[TrackType.VIDEO].getOrNull(index) ?: return null
+        source.init()
+        log.i("tryCreateSegment(${TrackType.VIDEO}, $index): created!")
+        if (tracks.active.has(TrackType.VIDEO)) {
+            source.selectTrack(TrackType.VIDEO)
+        }
+        // Update current index before pipeline creation, for other components
+        // who check it during pipeline init.
+        currentIndex[TrackType.VIDEO] = index
+        val pipeline = factory(
+            TrackType.VIDEO,
+            index,
+            tracks.all[TrackType.VIDEO],
+            tracks.outputFormats[TrackType.VIDEO]
+        )
+        return Segment(TrackType.VIDEO, index, pipeline)
+    }
+
+    private fun destroySegment(id: String? = null) {
+        currentSegment?.let {
+            it.release()
+            val source = sources[it.type][it.index]
+            if (tracks.active.has(it.type)) {
+                source.releaseTrack(it.type)
+            }
+        }
+        if (id == null) {
+            segmentMap.clear()
+        }
+        else {
+            segmentMap[id] = null
+        }
+    }
+    private fun DataSource.init() = if (!isInitialized) initialize() else Unit
+
+    private fun DataSource.deinit() = if (isInitialized) deinitialize() else Unit
+
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
index 97caa630..5dea66d0 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
@@ -28,20 +28,21 @@ class DataSources constructor(
         audioSources.init()
     }
 
-    private val videoSources: List<DataSource> = run {
-        val valid = videoSources.count { it.getTrackFormat(TrackType.VIDEO) != null }
-        when (valid) {
-            0 -> listOf<DataSource>().also { videoSources.deinit() }
-            videoSources.size -> videoSources
-            else -> videoSources // Tracks will crash
+    private var videoSources: List<DataSource> = updateVideoSources(videoSources)
+        set(value) {
+            field = updateVideoSources(value)
+        }
+
+    private var audioSources: List<DataSource> = updateAudioSources(audioSources)
+        set(value) {
+            field = updateAudioSources(value)
         }
-    }
 
-    private val audioSources: List<DataSource> = run {
-        val valid = audioSources.count { it.getTrackFormat(TrackType.AUDIO) != null }
-        when (valid) {
-            0 -> listOf<DataSource>().also { audioSources.deinit() }
-            audioSources.size -> audioSources
+    private fun updateAudioSources(sources: List<DataSource>) : List<DataSource> {
+        val valid = sources.count { it.getTrackFormat(TrackType.AUDIO) != null }
+        return when (valid) {
+            0 -> listOf<DataSource>().also { sources.deinit() }
+            sources.size -> sources
             else -> {
                 // Some tracks do not have audio, while some do. Replace with BlankAudio.
                 audioSources.map { source ->
@@ -52,6 +53,55 @@ class DataSources constructor(
         }
     }
 
+    private fun updateVideoSources(sources: List<DataSource>): List<DataSource> {
+        val valid = sources.count { it.getTrackFormat(TrackType.VIDEO) != null }
+        return when (valid) {
+            0 -> listOf<DataSource>().also { sources.deinit() }
+            sources.size -> sources
+            else -> sources // Tracks will crash
+        }
+    }
+
+    fun addDataSource(dataSource: DataSource) {
+        addVideoDataSource(dataSource)
+        addAudioDataSource(dataSource)
+    }
+
+    fun addVideoDataSource(dataSource: DataSource) {
+        dataSource.init()
+        if (dataSource.getTrackFormat(TrackType.VIDEO) != null && dataSource !in videoSources) {
+            videoSources = videoSources + dataSource
+        }
+    }
+    fun addAudioDataSource(dataSource: DataSource) {
+        dataSource.init()
+        if (dataSource.getTrackFormat(TrackType.AUDIO) != null) {
+            audioSources = audioSources + dataSource
+        }
+    }
+
+    fun removeDataSource(dataSourceId: String) {
+        removeAudioDataSource(dataSourceId)
+        removeVideoDataSource(dataSourceId)
+    }
+
+    fun removeVideoDataSource(dataSourceId: String) {
+        val source = videoSources.find { it.mediaId() == dataSourceId }
+        if (source?.getTrackFormat(TrackType.VIDEO) != null) {
+            videoSources = videoSources - source
+            source.releaseTrack(TrackType.VIDEO)
+        }
+        source?.deinit()
+    }
+
+    fun removeAudioDataSource(dataSourceId: String) {
+        val source = audioSources.find { it.mediaId() == dataSourceId }
+        if (source?.getTrackFormat(TrackType.AUDIO) != null) {
+            audioSources = audioSources - source
+            source.releaseTrack(TrackType.AUDIO)
+        }
+        source?.deinit()
+    }
     override fun get(type: TrackType) = when (type) {
         TrackType.AUDIO -> audioSources
         TrackType.VIDEO -> videoSources
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt
index 44fd586d..ea892023 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt
@@ -11,19 +11,23 @@ import com.otaliastudios.transcoder.source.DataSource
 import com.otaliastudios.transcoder.strategy.TrackStrategy
 
 class Tracks(
-    strategies: TrackMap<TrackStrategy>,
-    sources: DataSources,
-    videoRotation: Int,
-    forceCompression: Boolean
+    val strategies: TrackMap<TrackStrategy>,
+    val sources: DataSources,
+    val videoRotation: Int,
+    val forceCompression: Boolean
 ) {
 
     private val log = Logger("Tracks")
 
-    val all: TrackMap<TrackStatus>
-
-    val outputFormats: TrackMap<MediaFormat>
+    lateinit var all: TrackMap<TrackStatus>
+    lateinit var outputFormats: TrackMap<MediaFormat>
+    lateinit var active: TrackMap<TrackStatus>
 
     init {
+        updateTracksInfo()
+    }
+
+    fun updateTracksInfo() {
         val (audioFormat, audioStatus) = resolveTrack(TrackType.AUDIO, strategies.audio, sources.audioOrNull())
         val (videoFormat, videoStatus) = resolveTrack(TrackType.VIDEO, strategies.video, sources.videoOrNull())
         all = trackMapOf(
@@ -33,12 +37,13 @@ class Tracks(
         outputFormats = trackMapOf(video = videoFormat, audio = audioFormat)
         log.i("init: videoStatus=$videoStatus, resolvedVideoStatus=${all.video}, videoFormat=$videoFormat")
         log.i("init: audioStatus=$audioStatus, resolvedAudioStatus=${all.audio}, audioFormat=$audioFormat")
+
+        active = trackMapOf(
+            video = all.video.takeIf { it.isTranscoding },
+            audio = all.audio.takeIf { it.isTranscoding }
+        )
     }
 
-    val active: TrackMap<TrackStatus> = trackMapOf(
-        video = all.video.takeIf { it.isTranscoding },
-        audio = all.audio.takeIf { it.isTranscoding }
-    )
 
     private fun resolveVideoStatus(status: TrackStatus, forceCompression: Boolean, rotation: Int): TrackStatus {
         val force = forceCompression || rotation != 0
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index c2ae7066..32b2be69 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -5,8 +5,8 @@ package com.otaliastudios.transcoder.internal.thumbnails
 import android.media.MediaFormat
 import com.otaliastudios.transcoder.common.TrackStatus
 import com.otaliastudios.transcoder.common.TrackType
+import com.otaliastudios.transcoder.internal.CustomSegments
 import com.otaliastudios.transcoder.internal.DataSources
-import com.otaliastudios.transcoder.internal.Segments
 import com.otaliastudios.transcoder.internal.Timer
 import com.otaliastudios.transcoder.internal.Tracks
 import com.otaliastudios.transcoder.internal.codec.Decoder
@@ -60,7 +60,7 @@ class DefaultThumbnailsEngine(
         true
     )
 
-    private val segments = Segments(dataSources, tracks, ::createPipeline, false)
+    private val segments = CustomSegments(dataSources, tracks, ::createPipeline)
 
     private val timer = Timer(DefaultTimeInterpolator(), dataSources, tracks, segments.currentIndex)
 
@@ -109,7 +109,9 @@ class DefaultThumbnailsEngine(
         outputFormat: MediaFormat
     ): Pipeline {
         val source = dataSources[type][index].ignoringEOS()
-
+        if(VERBOSE) {
+            log.i("Creating pipeline #$index. absoluteUs=${stubs.joinToString { it.toString() }}")
+        }
         return Pipeline.build("Thumbnails") {
             Seeker(source) {
                 var seek = false
@@ -219,19 +221,57 @@ class DefaultThumbnailsEngine(
         return nextKeyFrameIndex
     }
 
+    override  fun addDataSource(dataSource: DataSource) {
+        dataSources.addVideoDataSource(dataSource)
+        tracks.updateTracksInfo()
+        if (tracks.active.has(TrackType.VIDEO)) {
+            dataSource.selectTrack(TrackType.VIDEO)
+        }
+    }
+
+    override fun removeDataSource(dataSourceId: String) {
+        dataSources.removeVideoDataSource(dataSourceId)
+        tracks.updateTracksInfo()
+    }
+
     override suspend fun queueThumbnails(list: List<ThumbnailRequest>) {
-        val segment = segments.next(TrackType.VIDEO)
-        segment?.let {
-            this.updatePositions(list, it.index)
+
+        val map = list.groupBy { it.sourceId() }
+
+        map.forEach { entry ->
+            val positions = entry.value.flatMap { request ->
+                val duration = timer.totalDurationUs
+                request.locate(duration).map { it to request }
+            }.sortedBy { it.first }
+            val index = dataSources[TrackType.VIDEO].indexOfFirst { it.mediaId() == entry.key }
+            if (index >= 0) {
+                stubs.addAll(
+                    positions.map { (positionUs, request) ->
+                        Stub(request, positionUs, positionUs)
+                    }.toMutableList().reorder(dataSources[TrackType.VIDEO][index])
+                )
+            }
+            if (VERBOSE) {
+                log.i("Updating pipeline positions for segment Index#$index absoluteUs=${positions.joinToString { it.first.toString() }}, and stubs $stubs")
+            }
         }
-        while (currentCoroutineContext().isActive) {
-            val advanced = segments.next(TrackType.VIDEO)?.advance() ?: false
-            val completed = !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
-            if (completed || stubs.isEmpty()) {
-                log.i("loop broken $stubs")
-                break
-            } else if (!advanced) {
-                delay(WAIT_MS)
+
+        if (stubs.isNotEmpty()) {
+            while (currentCoroutineContext().isActive) {
+                val segment =
+                    stubs.firstOrNull()?.request?.sourceId()?.let { segments.getSegment(it) }
+                if (VERBOSE) {
+                    log.i("loop advancing for $segment")
+                }
+                val advanced = segment?.advance() ?: false
+                // avoid calling hasNext if we advanced.
+                val completed = !advanced && !segments.hasNext()
+                if (completed || stubs.isEmpty()) {
+                    log.i("loop broken $stubs")
+                    break
+                } else if (!advanced) {
+                    delay(WAIT_MS)
+                }
             }
         }
     }
@@ -241,12 +281,12 @@ class DefaultThumbnailsEngine(
         segments.release()
     }
 
-    override suspend fun removePosition(positionUs: Long) {
-        if (positionUs == stubs.firstOrNull()?.positionUs) {
+    override suspend fun removePosition(source: String, positionUs: Long) {
+        if (stubs.firstOrNull()?.request?.sourceId() == source && positionUs == stubs.firstOrNull()?.positionUs) {
             return
         }
         val locatedTimestampUs = SingleThumbnailRequest(positionUs).locate(timer.durationUs.video)[0]
-        val stub = stubs.find { it.positionUs == locatedTimestampUs }
+        val stub = stubs.find {it.request.sourceId() == source &&  it.positionUs == locatedTimestampUs }
         if (stub != null) {
             log.i("removePosition Match: $positionUs :$stubs")
             stubs.remove(stub)
@@ -254,22 +294,6 @@ class DefaultThumbnailsEngine(
         }
     }
 
-    private fun updatePositions(requests: List<ThumbnailRequest>, index: Int) {
-        val positions = requests.flatMap { request ->
-            val duration = timer.totalDurationUs
-            request.locate(duration).map { it to request }
-        }
-        log.i("Creating pipeline #$index. absoluteUs=${positions.joinToString { it.first.toString() }}")
-
-        stubs.addAll(
-            positions.mapNotNull { (positionUs, request) ->
-                val localizedUs = timer.localize(TrackType.VIDEO, index, positionUs)
-                localizedUs?.let { Stub(request, positionUs, localizedUs) }
-//            }.toMutableList().sortedBy { it.positionUs }
-            }.toMutableList().reorder(dataSources[TrackType.VIDEO][0])
-        )
-    }
-
     private fun  List<Stub>.reorder(source: DataSource): Collection<Stub> {
         val bucketListMap = LinkedHashMap<Long, ArrayList<Stub>>()
         val finalList = ArrayList<Stub>()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index 8d4c5c3d..dea669df 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -5,6 +5,7 @@ package com.otaliastudios.transcoder.internal.thumbnails
 import com.otaliastudios.transcoder.ThumbnailerOptions
 import com.otaliastudios.transcoder.internal.DataSources
 import com.otaliastudios.transcoder.internal.utils.Logger
+import com.otaliastudios.transcoder.source.DataSource
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
 import com.otaliastudios.transcoder.thumbnail.ThumbnailRequest
 import kotlinx.coroutines.flow.Flow
@@ -13,9 +14,13 @@ abstract class ThumbnailsEngine {
 
     abstract val progressFlow: Flow<Thumbnail>
 
+    abstract fun addDataSource(dataSource: DataSource)
+
+    abstract fun removeDataSource(dataSourceId: String)
+
     abstract suspend fun queueThumbnails(list: List<ThumbnailRequest>)
 
-    abstract suspend fun removePosition(positionUs: Long)
+    abstract suspend fun removePosition(source: String, positionUs: Long)
 
     abstract fun cleanup()
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/Thumbnail.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/Thumbnail.kt
index dcebe09c..fe96f706 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/Thumbnail.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/Thumbnail.kt
@@ -2,7 +2,7 @@ package com.otaliastudios.transcoder.thumbnail
 
 import android.graphics.Bitmap
 
-class Thumbnail internal constructor(
+class Thumbnail constructor(
     val request: ThumbnailRequest,
     val positionUs: Long,
     val bitmap: Bitmap

From 6515fc645e3ee60c2f8ccb466c0adcc0fc936d37 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Wed, 11 May 2022 00:31:28 +0530
Subject: [PATCH 057/101] destroy and create logic fix for custom segments
 (#15)

---
 .../transcoder/internal/CustomSegments.kt     | 20 ++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
index e2546335..c189b9e6 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
@@ -18,6 +18,7 @@ class CustomSegments(
 
     private val log = Logger("Segments")
     private var currentSegment: Segment? = null
+    private var currentSegmentMapKey: String? = null
     val currentIndex = mutableTrackMapOf(-1, -1)
     private val segmentMap = mutableMapOf<String, Segment?>()
 
@@ -43,14 +44,15 @@ class CustomSegments(
 
     fun getSegment(id: String): Segment? {
         return segmentMap.getOrPut(id) {
-            destroySegment(id)
+            destroySegment()
             tryCreateSegment(id).also {
                 currentSegment = it
+                currentSegmentMapKey = id
             }
         }
     }
 
-    fun release() = destroySegment()
+    fun release() = destroySegment(true)
 
     private fun tryCreateSegment(id: String): Segment? {
         val index = sources[TrackType.VIDEO].indexOfFirst { it.mediaId() == id }
@@ -74,19 +76,19 @@ class CustomSegments(
         return Segment(TrackType.VIDEO, index, pipeline)
     }
 
-    private fun destroySegment(id: String? = null) {
+    private fun destroySegment(releaseAll: Boolean = false) {
         currentSegment?.let {
             it.release()
             val source = sources[it.type][it.index]
             if (tracks.active.has(it.type)) {
                 source.releaseTrack(it.type)
             }
-        }
-        if (id == null) {
-            segmentMap.clear()
-        }
-        else {
-            segmentMap[id] = null
+            currentSegmentMapKey?.let {
+                segmentMap[it] = null
+            }
+            if(releaseAll) {
+                segmentMap.clear()
+            }
         }
     }
     private fun DataSource.init() = if (!isInitialized) initialize() else Unit

From 95c45078563533d845ece55c250a3733ccdddca4 Mon Sep 17 00:00:00 2001
From: akshay-v9 <88997287+akshay-v9@users.noreply.github.com>
Date: Thu, 19 May 2022 00:21:56 +0530
Subject: [PATCH 058/101] Prevents to create negative timeStamp for endTime in
 TrimDataSource (#17)

---
 .../com/otaliastudios/transcoder/source/ClipDataSource.java     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/ClipDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/ClipDataSource.java
index 48a05bbd..485c11d0 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/ClipDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/ClipDataSource.java
@@ -15,7 +15,7 @@ public ClipDataSource(@NonNull DataSource source, long clipStartUs) {
     public ClipDataSource(@NonNull DataSource source, long clipStartUs, long clipEndUs) {
         super(new TrimDataSource(source,
                 clipStartUs,
-                getSourceDurationUs(source) - clipEndUs));
+                Math.max(getSourceDurationUs(source) - clipEndUs, 0L)));
     }
 
     private static long getSourceDurationUs(@NonNull DataSource source) {

From 0aeaf975bc5ce55907b685c5c7b29ccc1c910a54 Mon Sep 17 00:00:00 2001
From: akshar-bhatnagar <87642541+akshar-bhatnagar@users.noreply.github.com>
Date: Thu, 19 May 2022 00:24:04 +0530
Subject: [PATCH 059/101] fixes duplicate data source addition to thumbnailer
 (#16)

* fixes duplicate data source addition to thumbnailer

* init dataSource after checking if it already exists

* adds check for audioSources

Co-authored-by: Akshar Bhatnagar <akshar@Akshars-MacBook-Pro.local>
---
 .../com/otaliastudios/transcoder/internal/DataSources.kt    | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
index 5dea66d0..3a4a36c4 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
@@ -62,18 +62,24 @@ class DataSources constructor(
         }
     }
 
+    private fun existsInSources(dataSource: DataSource, sources: List<DataSource>): Boolean {
+        return (sources.find { it.mediaId() == dataSource.mediaId() }) != null
+    }
+
     fun addDataSource(dataSource: DataSource) {
         addVideoDataSource(dataSource)
         addAudioDataSource(dataSource)
     }
 
     fun addVideoDataSource(dataSource: DataSource) {
+        if (existsInSources(dataSource, videoSources)) return
         dataSource.init()
         if (dataSource.getTrackFormat(TrackType.VIDEO) != null && dataSource !in videoSources) {
             videoSources = videoSources + dataSource
         }
     }
     fun addAudioDataSource(dataSource: DataSource) {
+        if (existsInSources(dataSource, audioSources)) return
         dataSource.init()
         if (dataSource.getTrackFormat(TrackType.AUDIO) != null) {
             audioSources = audioSources + dataSource

From 6263f331e09b043b962d3be9da40dd369bf88f29 Mon Sep 17 00:00:00 2001
From: akshar-bhatnagar <87642541+akshar-bhatnagar@users.noreply.github.com>
Date: Thu, 19 May 2022 12:15:59 +0530
Subject: [PATCH 060/101] fixes crash due to selectTrack on dataSource already
 added (#18)

* fixes crash due to selectTrack on dataSource already added

* checks sources in DefaultThumbnailsEngine, reverts checking in DataSources

Co-authored-by: Akshar Bhatnagar <akshar@Akshars-MacBook-Pro.local>
---
 .../com/otaliastudios/transcoder/internal/DataSources.kt | 9 +++------
 .../internal/thumbnails/DefaultThumbnailsEngine.kt       | 5 ++++-
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
index 3a4a36c4..1c54868d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
@@ -62,30 +62,27 @@ class DataSources constructor(
         }
     }
 
-    private fun existsInSources(dataSource: DataSource, sources: List<DataSource>): Boolean {
-        return (sources.find { it.mediaId() == dataSource.mediaId() }) != null
-    }
-
     fun addDataSource(dataSource: DataSource) {
         addVideoDataSource(dataSource)
         addAudioDataSource(dataSource)
     }
 
     fun addVideoDataSource(dataSource: DataSource) {
-        if (existsInSources(dataSource, videoSources)) return
         dataSource.init()
         if (dataSource.getTrackFormat(TrackType.VIDEO) != null && dataSource !in videoSources) {
             videoSources = videoSources + dataSource
         }
     }
+
     fun addAudioDataSource(dataSource: DataSource) {
-        if (existsInSources(dataSource, audioSources)) return
         dataSource.init()
         if (dataSource.getTrackFormat(TrackType.AUDIO) != null) {
             audioSources = audioSources + dataSource
         }
     }
 
+    fun getVideoSources() = videoSources
+
     fun removeDataSource(dataSourceId: String) {
         removeAudioDataSource(dataSourceId)
         removeVideoDataSource(dataSourceId)
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 32b2be69..93544cad 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -221,7 +221,10 @@ class DefaultThumbnailsEngine(
         return nextKeyFrameIndex
     }
 
-    override  fun addDataSource(dataSource: DataSource) {
+    override fun addDataSource(dataSource: DataSource) {
+        if (dataSources.getVideoSources().find { it.mediaId() == dataSource.mediaId() } != null) {
+            return // dataSource already exists
+        }
         dataSources.addVideoDataSource(dataSource)
         tracks.updateTracksInfo()
         if (tracks.active.has(TrackType.VIDEO)) {

From 92f72f64c31655e6352a6e219a74894aade296ba Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Mon, 23 May 2022 15:38:36 +0530
Subject: [PATCH 061/101] Feature/remove queued fix and destroy segment (#19)

* fixes removing all queued ts logic, adds destroy segment with id

* clear stubs on cleanup

Co-authored-by: Akshar Bhatnagar <akshar@Akshars-MacBook-Pro.local>
---
 .../transcoder/internal/CustomSegments.kt            | 12 ++++++++++++
 .../internal/thumbnails/DefaultThumbnailsEngine.kt   |  7 +++++++
 2 files changed, 19 insertions(+)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
index c189b9e6..1c1d2435 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
@@ -52,6 +52,18 @@ class CustomSegments(
         }
     }
 
+    fun releaseSegment(id: String) {
+        val segment = segmentMap[id]
+        segment?.let {
+            it.release()
+            val source = sources[it.type][it.index]
+            if (tracks.active.has(it.type)) {
+                source.releaseTrack(it.type)
+            }
+            segmentMap[id] = null
+        }
+    }
+
     fun release() = destroySegment(true)
 
     private fun tryCreateSegment(id: String): Segment? {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 93544cad..74ff367c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -233,6 +233,7 @@ class DefaultThumbnailsEngine(
     }
 
     override fun removeDataSource(dataSourceId: String) {
+        segments.releaseSegment(dataSourceId)
         dataSources.removeVideoDataSource(dataSourceId)
         tracks.updateTracksInfo()
     }
@@ -285,6 +286,11 @@ class DefaultThumbnailsEngine(
     }
 
     override suspend fun removePosition(source: String, positionUs: Long) {
+        if (positionUs < 0) {
+            stubs.removeAll{
+                it.request.sourceId() == source
+            }
+        }
         if (stubs.firstOrNull()?.request?.sourceId() == source && positionUs == stubs.firstOrNull()?.positionUs) {
             return
         }
@@ -320,6 +326,7 @@ class DefaultThumbnailsEngine(
     }
 
     override fun cleanup() {
+        runCatching { stubs.clear() }
         runCatching { segments.release() }
         runCatching { dataSources.release() }
     }

From b2494d1550e881780ad339c24fc7cfee1dfe1267 Mon Sep 17 00:00:00 2001
From: akshar-bhatnagar <87642541+akshar-bhatnagar@users.noreply.github.com>
Date: Mon, 23 May 2022 19:43:11 +0530
Subject: [PATCH 062/101] returns when all requests for a source are removed,
 prevents locate() crash (#20)

Co-authored-by: Akshar Bhatnagar <akshar@Akshars-MacBook-Pro.local>
---
 .../transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt    | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 74ff367c..bbbf67e5 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -290,6 +290,7 @@ class DefaultThumbnailsEngine(
             stubs.removeAll{
                 it.request.sourceId() == source
             }
+            return
         }
         if (stubs.firstOrNull()?.request?.sourceId() == source && positionUs == stubs.firstOrNull()?.positionUs) {
             return

From fc62a06e0307e11a2aaa851200c8c279920ab566 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Tue, 24 May 2022 15:53:14 +0530
Subject: [PATCH 063/101] try catch exception after media source removed (#21)

---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt    | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index bbbf67e5..de838d59 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -26,9 +26,11 @@ import com.otaliastudios.transcoder.thumbnail.SingleThumbnailRequest
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
 import com.otaliastudios.transcoder.thumbnail.ThumbnailRequest
 import com.otaliastudios.transcoder.time.DefaultTimeInterpolator
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.isActive
@@ -267,7 +269,14 @@ class DefaultThumbnailsEngine(
                 if (VERBOSE) {
                     log.i("loop advancing for $segment")
                 }
-                val advanced = segment?.advance() ?: false
+                val advanced = try {
+                    segment?.advance() ?: false
+                } catch (e: Exception) {
+                    if (e !is CancellationException) {
+                        currentCoroutineContext().ensureActive()
+                    }
+                    throw e
+                }
                 // avoid calling hasNext if we advanced.
                 val completed = !advanced && !segments.hasNext()
                 if (completed || stubs.isEmpty()) {

From e5ab652e632ffd86e5af5e82725c99b89e86d7b8 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Tue, 7 Jun 2022 14:52:25 +0530
Subject: [PATCH 064/101] handle negative timestamp in encoder on EOS (#22)

---
 .../com/otaliastudios/transcoder/internal/codec/Encoder.kt     | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt
index 6719ab3c..87a63e59 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Encoder.kt
@@ -143,10 +143,11 @@ class Encoder(
                     val isEos = info.flags and BUFFER_FLAG_END_OF_STREAM != 0
                     val flags = info.flags and BUFFER_FLAG_END_OF_STREAM.inv()
                     val buffer = buffers.getOutputBuffer(result)
-                    val timeUs = info.presentationTimeUs
+                    val timeUs = if (info.presentationTimeUs < 0) 0 else info.presentationTimeUs
                     if (isEos && timeUs == 0L) {
                         info.offset = 0
                         info.size = 0
+                        info.presentationTimeUs = 0
                     }
                     buffer.clear()
                     buffer.limit(info.offset + info.size)

From ef1c968431de3bbfd6f88533150b2240c35778d1 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Fri, 24 Jun 2022 14:53:13 +0530
Subject: [PATCH 065/101] decoder illegal state exception when removing a
 datasource (#23)

---
 .../otaliastudios/transcoder/internal/CustomSegments.kt    | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
index 1c1d2435..de69802d 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
@@ -60,7 +60,10 @@ class CustomSegments(
             if (tracks.active.has(it.type)) {
                 source.releaseTrack(it.type)
             }
-            segmentMap[id] = null
+            segmentMap.remove(id)
+            if(currentSegment == segment) {
+                currentSegment = null
+            }
         }
     }
 
@@ -96,7 +99,7 @@ class CustomSegments(
                 source.releaseTrack(it.type)
             }
             currentSegmentMapKey?.let {
-                segmentMap[it] = null
+                segmentMap.remove(it)
             }
             if(releaseAll) {
                 segmentMap.clear()

From 7e36a86534edd48957440f3ac1ed6f7a36fdad18 Mon Sep 17 00:00:00 2001
From: Sahil Bajaj <sahilbajaj.nc@gmail.com>
Date: Mon, 27 Jun 2022 17:02:40 +0530
Subject: [PATCH 066/101] Use software codec for 4k videos (#24)

* Use software codec for 4k videos

* Fixes 4k

Co-authored-by: Sahil Bajaj <bajaj@192.168.1.32>
---
 .../transcoder/internal/codec/Decoder.kt      | 70 +++++++++++++++++--
 .../thumbnails/DefaultThumbnailsEngine.kt     |  2 +-
 2 files changed, 64 insertions(+), 8 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index 478dd3f5..89bc151a 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -1,13 +1,11 @@
 package com.otaliastudios.transcoder.internal.codec
 
 import android.media.MediaCodec
-import android.media.MediaCodec.BUFFER_FLAG_END_OF_STREAM
-import android.media.MediaCodec.BUFFER_FLAG_SYNC_FRAME
-import android.media.MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED
-import android.media.MediaCodec.INFO_OUTPUT_FORMAT_CHANGED
-import android.media.MediaCodec.INFO_TRY_AGAIN_LATER
-import android.media.MediaCodec.createDecoderByType
+import android.media.MediaCodec.*
+import android.media.MediaCodecInfo
+import android.media.MediaCodecList
 import android.media.MediaFormat
+import android.os.Build
 import android.view.Surface
 import com.otaliastudios.transcoder.common.trackType
 import com.otaliastudios.transcoder.internal.data.ReaderChannel
@@ -36,6 +34,7 @@ interface DecoderChannel : Channel {
 class Decoder(
     private val format: MediaFormat, // source.getTrackFormat(track)
     continuous: Boolean, // relevant if the source sends no-render chunks. should we compensate or not?
+    useSwFor4K: Boolean = false,
     val shouldFlush: (() -> Boolean)? = null
 ) : QueuedStep<ReaderData, ReaderChannel, DecoderData, DecoderChannel>(), ReaderChannel {
 
@@ -48,7 +47,64 @@ class Decoder(
     private val log = Logger("Decoder(${format.trackType},${ID[format.trackType].getAndIncrement()})")
     override val channel = this
 
-    private val codec = createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!)
+    private val codec = createDecoderByType(format, useSwFor4K && format.is4K())
+
+    @Suppress("MagicNumber")
+    private fun MediaFormat.is4K(): Boolean {
+        val width = format.getInteger(MediaFormat.KEY_WIDTH)
+        val height = format.getInteger(MediaFormat.KEY_HEIGHT)
+        return width * height > 2120 * 2120
+    }
+
+    private fun MediaCodecInfo.isHardwareAcceleratedCompat() : Boolean {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            isHardwareAccelerated
+        } else {
+            val codecName = name.lowercase()
+            val isSoftware = (codecName.startsWith("omx.google.")
+                    || codecName.startsWith("omx.ffmpeg.")
+                    || (codecName.startsWith("omx.sec.") && codecName.contains(".sw."))
+                    || codecName == "omx.qcom.video.decoder.hevcswvdec"
+                    || codecName.startsWith("c2.android.")
+                    || codecName.startsWith("c2.google.")
+                    || (!codecName.startsWith("omx.") && !codecName.startsWith("c2.")))
+
+            if (isSoftware) {
+                log.i("sw codec: $name")
+            }
+            !isSoftware
+        }
+    }
+
+    private fun createDecoderByType(format: MediaFormat, useSoftware: Boolean = false): MediaCodec {
+        val mime = format.getString(MediaFormat.KEY_MIME)!!
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            val allCodecs = MediaCodecList(MediaCodecList.ALL_CODECS)
+
+            var codecName: String? = null
+            for (info in allCodecs.codecInfos) {
+                if (info.isEncoder || (info.isHardwareAcceleratedCompat() && useSoftware)) {
+                    // log.e("Rejecting codec: ${info.name}")
+                    continue
+                }
+                try {
+                    val caps = info.getCapabilitiesForType(mime)
+                    if (caps != null && caps.isFormatSupported(format)) {
+                        codecName = info.name
+                        break
+                    } else {
+                        // log.e("Rejecting decoder: ${info.name}")
+                    }
+                } catch (e: IllegalArgumentException) {
+                    log.e("Unsupported codec type: $mime")
+                }
+            }
+            log.i("Using codec: $codecName for format: $format")
+            return createByCodecName(codecName!!)
+        }
+        return createDecoderByType(mime)
+    }
+
     private val buffers by lazy { MediaCodecBuffers(codec) }
     private var info = MediaCodec.BufferInfo()
     private val dropper = DecoderDropper(continuous)
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index de838d59..91e3fc66 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -150,7 +150,7 @@ class DefaultThumbnailsEngine(
                 Pair(seekUs, seek)
             } +
                 Reader(source, type) +
-                Decoder(source.getTrackFormat(type)!!, continuous = false) {
+                Decoder(source.getTrackFormat(type)!!, continuous = false, useSwFor4K = true) {
                     shouldFlush.also {
                         shouldFlush = false
                     }

From c346f4b86ffb867db00907eb17787457c3d0a3c7 Mon Sep 17 00:00:00 2001
From: akshar-bhatnagar <87642541+akshar-bhatnagar@users.noreply.github.com>
Date: Tue, 28 Jun 2022 17:22:38 +0530
Subject: [PATCH 067/101] adds updateDataSources functions to thumbnail engine
 (#25)

* adds updateDataSources functions to thumbnail engine

* uses existing add and removeDataSource to simplify adding/removing

Co-authored-by: Akshar Bhatnagar <akshar@Akshars-MacBook-Pro.local>
---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt | 14 ++++++++++++++
 .../internal/thumbnails/ThumbnailsEngine.kt        |  2 ++
 2 files changed, 16 insertions(+)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 91e3fc66..90f90189 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -240,6 +240,20 @@ class DefaultThumbnailsEngine(
         tracks.updateTracksInfo()
     }
 
+    override fun updateDataSources(dataSourcesNew: List<DataSource>) {
+        val currentVideoIds = dataSources.videoOrNull()?.map { it.mediaId() }
+        val newSourceIds = dataSourcesNew.map { it.mediaId() }.distinct()
+        val toAdd = newSourceIds - currentVideoIds
+        val toRemove = currentVideoIds?.minus(newSourceIds)
+        toAdd.forEach { id ->
+            val source = dataSourcesNew.first { it.mediaId() == id }
+            addDataSource(source)
+        }
+        toRemove?.forEach { id ->
+            removeDataSource(id)
+        }
+    }
+
     override suspend fun queueThumbnails(list: List<ThumbnailRequest>) {
 
         val map = list.groupBy { it.sourceId() }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index dea669df..326d8012 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -18,6 +18,8 @@ abstract class ThumbnailsEngine {
 
     abstract fun removeDataSource(dataSourceId: String)
 
+    abstract fun updateDataSources(dataSources: List<DataSource>)
+
     abstract suspend fun queueThumbnails(list: List<ThumbnailRequest>)
 
     abstract suspend fun removePosition(source: String, positionUs: Long)

From 7b12b870101e7847913f4dbda3c5a61044d50856 Mon Sep 17 00:00:00 2001
From: akshay-v9 <88997287+akshay-v9@users.noreply.github.com>
Date: Thu, 30 Jun 2022 18:20:00 +0530
Subject: [PATCH 068/101] added stream create/destroy support in audio
 resampler (#26)

---
 .../transcoder/internal/audio/AudioEngine.kt          |  2 ++
 .../transcoder/resample/AudioResampler.java           | 11 +++++++++++
 .../transcoder/resample/DefaultAudioResampler.java    | 10 ++++++++++
 .../transcoder/resample/DownsampleAudioResampler.java | 10 ++++++++++
 .../resample/PassThroughAudioResampler.java           | 10 ++++++++++
 .../transcoder/resample/UpsampleAudioResampler.java   | 10 ++++++++++
 6 files changed, 53 insertions(+)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
index 928431c6..de3b2362 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
@@ -53,12 +53,14 @@ class AudioEngine(
         this.rawFormat = rawFormat
         remixer = AudioRemixer[rawFormat.channels, targetFormat.channels]
         chunks = ChunkQueue(rawFormat.sampleRate, rawFormat.channels)
+        resampler.createStream(rawFormat.sampleRate, targetFormat.sampleRate, targetFormat.channels)
     }
 
     override fun enqueueEos(data: DecoderData) {
         log.i("enqueueEos()")
         data.release(false)
         chunks.enqueueEos()
+        resampler.destroyStream()
     }
 
     override fun enqueue(data: DecoderData) {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/resample/AudioResampler.java b/lib/src/main/java/com/otaliastudios/transcoder/resample/AudioResampler.java
index 00c4290e..5ef8db62 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/resample/AudioResampler.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/resample/AudioResampler.java
@@ -22,6 +22,17 @@ public interface AudioResampler {
      */
     void resample(@NonNull final ShortBuffer inputBuffer, int inputSampleRate, @NonNull final ShortBuffer outputBuffer, int outputSampleRate, int channels);
 
+    /**
+     * createStream() and destroyStream() only to be implemented on Resamplers
+     * following a stream approach for continuous input buffers. Not for static one shot methods
+     * to resample.
+     * @param inputSampleRate the input sample rate
+     * @param outputSampleRate the output sample rate
+     * @param numChannels the number of channels
+     */
+    void createStream(int inputSampleRate, int outputSampleRate, int numChannels);
+    void destroyStream();
+
     AudioResampler DOWNSAMPLE = new DownsampleAudioResampler();
 
     AudioResampler UPSAMPLE = new UpsampleAudioResampler();
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/resample/DefaultAudioResampler.java b/lib/src/main/java/com/otaliastudios/transcoder/resample/DefaultAudioResampler.java
index 9d87c911..d8efb290 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/resample/DefaultAudioResampler.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/resample/DefaultAudioResampler.java
@@ -20,4 +20,14 @@ public void resample(@NonNull ShortBuffer inputBuffer, int inputSampleRate, @Non
             PASSTHROUGH.resample(inputBuffer, inputSampleRate, outputBuffer, outputSampleRate, channels);
         }
     }
+
+    @Override
+    public void createStream(int inputSampleRate, int outputSampleRate, int numChannels) {
+        // do nothing
+    }
+
+    @Override
+    public void destroyStream() {
+        // do nothing
+    }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/resample/DownsampleAudioResampler.java b/lib/src/main/java/com/otaliastudios/transcoder/resample/DownsampleAudioResampler.java
index b22bf137..3e4486b3 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/resample/DownsampleAudioResampler.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/resample/DownsampleAudioResampler.java
@@ -44,4 +44,14 @@ public void resample(@NonNull ShortBuffer inputBuffer, int inputSampleRate, @Non
             }
         }
     }
+
+    @Override
+    public void createStream(int inputSampleRate, int outputSampleRate, int numChannels) {
+        // do nothing
+    }
+
+    @Override
+    public void destroyStream() {
+        // do nothing
+    }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/resample/PassThroughAudioResampler.java b/lib/src/main/java/com/otaliastudios/transcoder/resample/PassThroughAudioResampler.java
index 58423c5d..baa12bb8 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/resample/PassThroughAudioResampler.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/resample/PassThroughAudioResampler.java
@@ -18,4 +18,14 @@ public void resample(@NonNull ShortBuffer inputBuffer, int inputSampleRate,
         }
         outputBuffer.put(inputBuffer);
     }
+
+    @Override
+    public void createStream(int inputSampleRate, int outputSampleRate, int numChannels) {
+        // do nothing
+    }
+
+    @Override
+    public void destroyStream() {
+        // do nothing
+    }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/resample/UpsampleAudioResampler.java b/lib/src/main/java/com/otaliastudios/transcoder/resample/UpsampleAudioResampler.java
index f88b6bcc..def9d107 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/resample/UpsampleAudioResampler.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/resample/UpsampleAudioResampler.java
@@ -45,6 +45,16 @@ public void resample(@NonNull ShortBuffer inputBuffer, int inputSampleRate, @Non
         }
     }
 
+    @Override
+    public void createStream(int inputSampleRate, int outputSampleRate, int numChannels) {
+        // do nothing
+    }
+
+    @Override
+    public void destroyStream() {
+        // do nothing
+    }
+
     /**
      * We have different options here.
      * 1. Return a 0 sample.

From cbde946e5ce4767337368d8c820ca98a2ecf1b0e Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Thu, 30 Jun 2022 20:00:55 +0530
Subject: [PATCH 069/101] make thumbnail engine non null (#27)

---
 .../java/com/otaliastudios/transcoder/ThumbnailerOptions.kt  | 2 +-
 .../transcoder/internal/thumbnails/ThumbnailsEngine.kt       | 5 ++---
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
index 713a5910..b92b42c8 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
@@ -101,7 +101,7 @@ class ThumbnailerOptions(
             )
         }
 
-        fun thumbnails(): ThumbnailsEngine? {
+        fun thumbnails(): ThumbnailsEngine {
             return ThumbnailsEngine.thumbnails(build())
         }
     }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index 326d8012..32b123c8 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -37,15 +37,14 @@ abstract class ThumbnailsEngine {
             }
         }
 
-        var engine: ThumbnailsEngine? = null
         private lateinit var dispatcher: ThumbnailsDispatcher
 
         @JvmStatic
-        fun thumbnails(options: ThumbnailerOptions): ThumbnailsEngine? {
+        fun thumbnails(options: ThumbnailerOptions): ThumbnailsEngine {
             log.i("thumbnails(): called...")
             dispatcher = ThumbnailsDispatcher(options)
 
-            engine = DefaultThumbnailsEngine(
+            val engine = DefaultThumbnailsEngine(
                 dataSources = DataSources(options),
                 rotation = options.rotation,
                 resizer = options.resizer

From ea65bd6f44a27621af4ed007b0573e0dcc054c80 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Fri, 15 Jul 2022 17:19:15 +0530
Subject: [PATCH 070/101] init decoder by name only if name not null (#28)

---
 .../com/otaliastudios/transcoder/internal/codec/Decoder.kt    | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index 89bc151a..0c60fb0a 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -100,7 +100,9 @@ class Decoder(
                 }
             }
             log.i("Using codec: $codecName for format: $format")
-            return createByCodecName(codecName!!)
+            if (codecName != null) {
+                return createByCodecName(codecName)
+            }
         }
         return createDecoderByType(mime)
     }

From ef35b6d3caee990442b05ae100396aa8d5e69c34 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Mon, 1 Aug 2022 02:07:32 -0700
Subject: [PATCH 071/101] disable logs in keyframes to fix stack overflow error
 (#30)

---
 .../otaliastudios/transcoder/source/DefaultDataSource.java    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index 7eddb98d..3fcab518 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -114,7 +114,7 @@ public long requestKeyFrameTimestamps() {
             mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + SEEK_THRESHOLD, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
             return -1;
         }
-        LOG.i("keyFrameStartTime:" + sampleTime);
+//        LOG.i("keyFrameStartTime:" + sampleTime);
 
         int count = 0;
         long lastSampleTime = -1L;
@@ -131,7 +131,7 @@ public long requestKeyFrameTimestamps() {
             sampleTime = mExtractor.getSampleTime();
             count++;
         }
-        LOG.i("keyFrameStopCount:" + keyFrameTimestamps);
+//        LOG.i("keyFrameStopCount:" + keyFrameTimestamps);
         return sampleTime;
     }
 

From 162f5290ba10c4f87f10b5e5f7b1c4a0edbfffbf Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Tue, 2 Aug 2022 16:46:21 +0530
Subject: [PATCH 072/101] disable decoder flush to fix buffer failed state
 (#33)

---
 .../com/otaliastudios/transcoder/internal/codec/Decoder.kt    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index 0c60fb0a..df515c29 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -126,8 +126,8 @@ class Decoder(
     }
 
     override fun buffer(): Pair<ByteBuffer, Int>? {
-        if (shouldFlush?.invoke() == true)
-            codec.flush()
+//        if (shouldFlush?.invoke() == true)
+//            codec.flush()
         val id = codec.dequeueInputBuffer(0)
         return if (id >= 0) {
             dequeuedInputs++

From 48739766004f29e0724d92371362bad2d8d157a9 Mon Sep 17 00:00:00 2001
From: Sahil Bajaj <sahilbajaj.nc@gmail.com>
Date: Tue, 2 Aug 2022 19:58:20 +0530
Subject: [PATCH 073/101] Fix decoder flush. Flush only if it's ready (#34)

Co-authored-by: Sahil Bajaj <bajaj@192.168.1.55>
---
 .../transcoder/internal/codec/Decoder.kt             | 12 +++++++++---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt   |  2 ++
 2 files changed, 11 insertions(+), 3 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index df515c29..47609166 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -49,6 +49,8 @@ class Decoder(
 
     private val codec = createDecoderByType(format, useSwFor4K && format.is4K())
 
+    private var decoderReady = false
+
     @Suppress("MagicNumber")
     private fun MediaFormat.is4K(): Boolean {
         val width = format.getInteger(MediaFormat.KEY_WIDTH)
@@ -123,12 +125,15 @@ class Decoder(
         val surface = next.handleSourceFormat(format)
         codec.configure(format, surface, null, 0)
         codec.start()
+        decoderReady = false
     }
 
     override fun buffer(): Pair<ByteBuffer, Int>? {
-//        if (shouldFlush?.invoke() == true)
-//            codec.flush()
-        val id = codec.dequeueInputBuffer(0)
+        if (shouldFlush?.invoke() == true && decoderReady) {
+            log.i("codec flush")
+            codec.flush()
+        }
+        val id = codec.dequeueInputBuffer(timeoutUs)
         return if (id >= 0) {
             dequeuedInputs++
             buffers.getInputBuffer(id) to id
@@ -168,6 +173,7 @@ class Decoder(
                     "drain(): got INFO_OUTPUT_FORMAT_CHANGED," +
                         " handling format and retrying. format=${codec.outputFormat}"
                 )
+                decoderReady = true
                 next.handleRawFormat(codec.outputFormat)
                 State.Retry
             }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 90f90189..7f7fa797 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -114,6 +114,8 @@ class DefaultThumbnailsEngine(
         if(VERBOSE) {
             log.i("Creating pipeline #$index. absoluteUs=${stubs.joinToString { it.toString() }}")
         }
+        shouldSeek = true
+        shouldFlush = false
         return Pipeline.build("Thumbnails") {
             Seeker(source) {
                 var seek = false

From 96e99ebccc932c1b341e81a86e194648eac896b1 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Tue, 2 Aug 2022 21:55:06 +0530
Subject: [PATCH 074/101] use localised duration for position mapping instead
 of global (#32)

---
 .../thumbnails/DefaultThumbnailsEngine.kt     | 44 ++++++++++++-------
 .../internal/thumbnails/ThumbnailsEngine.kt   |  2 +
 2 files changed, 30 insertions(+), 16 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 7f7fa797..9361fe44 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -261,21 +261,22 @@ class DefaultThumbnailsEngine(
         val map = list.groupBy { it.sourceId() }
 
         map.forEach { entry ->
-            val positions = entry.value.flatMap { request ->
-                val duration = timer.totalDurationUs
-                request.locate(duration).map { it to request }
-            }.sortedBy { it.first }
-            val index = dataSources[TrackType.VIDEO].indexOfFirst { it.mediaId() == entry.key }
-            if (index >= 0) {
+            val dataSource = getDataSourceForId(entry.key)
+            if (dataSource != null) {
+                val duration = dataSource.durationUs
+                val positions = entry.value.flatMap { request ->
+                    request.locate(duration).map { it to request }
+                }.sortedBy { it.first }
                 stubs.addAll(
                     positions.map { (positionUs, request) ->
                         Stub(request, positionUs, positionUs)
-                    }.toMutableList().reorder(dataSources[TrackType.VIDEO][index])
+                    }.toMutableList().reorder(dataSource)
                 )
+                if (VERBOSE) {
+                    log.i("Updating pipeline positions for segment source#$dataSource absoluteUs=${positions.joinToString { it.first.toString() }}, and stubs $stubs")
+                }
             }
-            if (VERBOSE) {
-                log.i("Updating pipeline positions for segment Index#$index absoluteUs=${positions.joinToString { it.first.toString() }}, and stubs $stubs")
-            }
+
         }
 
         if (stubs.isNotEmpty()) {
@@ -320,15 +321,26 @@ class DefaultThumbnailsEngine(
         if (stubs.firstOrNull()?.request?.sourceId() == source && positionUs == stubs.firstOrNull()?.positionUs) {
             return
         }
-        val locatedTimestampUs = SingleThumbnailRequest(positionUs).locate(timer.durationUs.video)[0]
-        val stub = stubs.find {it.request.sourceId() == source &&  it.positionUs == locatedTimestampUs }
-        if (stub != null) {
-            log.i("removePosition Match: $positionUs :$stubs")
-            stubs.remove(stub)
-            shouldSeek = true
+
+        val dataSource = getDataSourceForId(source)
+        if (dataSource != null) {
+            val duration = dataSource.durationUs
+            val locatedTimestampUs = SingleThumbnailRequest(positionUs).locate(duration)[0]
+            val stub =
+                stubs.find { it.request.sourceId() == source && it.positionUs == locatedTimestampUs }
+            if (stub != null) {
+                log.i("removePosition Match: $positionUs :$stubs")
+                stubs.remove(stub)
+                shouldSeek = true
+            }
         }
     }
 
+
+    override fun getDataSourceForId(source: String): DataSource? {
+        return dataSources[TrackType.VIDEO].firstOrNull { it.mediaId() == source }
+    }
+
     private fun  List<Stub>.reorder(source: DataSource): Collection<Stub> {
         val bucketListMap = LinkedHashMap<Long, ArrayList<Stub>>()
         val finalList = ArrayList<Stub>()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index 32b123c8..543aad32 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -24,6 +24,8 @@ abstract class ThumbnailsEngine {
 
     abstract suspend fun removePosition(source: String, positionUs: Long)
 
+    abstract fun getDataSourceForId(source: String): DataSource?
+
     abstract fun cleanup()
 
     companion object {

From bec18122b0ecf3dc240ff2bb01383304b962c55f Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Thu, 4 Aug 2022 17:13:48 +0530
Subject: [PATCH 075/101] remove active requests based on id and not path (#35)

* remove active requests based on id and not path

* get data source based on path not id
---
 .../thumbnails/DefaultThumbnailsEngine.kt     | 20 +++++++++----------
 .../internal/thumbnails/ThumbnailsEngine.kt   |  4 ++--
 .../transcoder/thumbnail/ThumbnailRequest.kt  |  2 ++
 3 files changed, 14 insertions(+), 12 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 9361fe44..bcc74493 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -77,7 +77,7 @@ class DefaultThumbnailsEngine(
     ) {
         var actualLocalizedUs: Long = localizedUs
         override fun toString(): String {
-            return request.sourceId() + ":" + positionUs.toString()
+            return request.sourcePath() + ":" + positionUs.toString()
         }
     }
 
@@ -258,10 +258,10 @@ class DefaultThumbnailsEngine(
 
     override suspend fun queueThumbnails(list: List<ThumbnailRequest>) {
 
-        val map = list.groupBy { it.sourceId() }
+        val map = list.groupBy { it.sourcePath() }
 
         map.forEach { entry ->
-            val dataSource = getDataSourceForId(entry.key)
+            val dataSource = getDataSourceByPath(entry.key)
             if (dataSource != null) {
                 val duration = dataSource.durationUs
                 val positions = entry.value.flatMap { request ->
@@ -282,7 +282,7 @@ class DefaultThumbnailsEngine(
         if (stubs.isNotEmpty()) {
             while (currentCoroutineContext().isActive) {
                 val segment =
-                    stubs.firstOrNull()?.request?.sourceId()?.let { segments.getSegment(it) }
+                    stubs.firstOrNull()?.request?.sourcePath()?.let { segments.getSegment(it) }
                 if (VERBOSE) {
                     log.i("loop advancing for $segment")
                 }
@@ -311,23 +311,23 @@ class DefaultThumbnailsEngine(
         segments.release()
     }
 
-    override suspend fun removePosition(source: String, positionUs: Long) {
+    override suspend fun removePosition(sourcePath: String, sourceId: String, positionUs: Long) {
         if (positionUs < 0) {
             stubs.removeAll{
-                it.request.sourceId() == source
+                it.request.sourceId() == sourceId
             }
             return
         }
-        if (stubs.firstOrNull()?.request?.sourceId() == source && positionUs == stubs.firstOrNull()?.positionUs) {
+        if (stubs.firstOrNull()?.request?.sourceId() == sourceId && positionUs == stubs.firstOrNull()?.positionUs) {
             return
         }
 
-        val dataSource = getDataSourceForId(source)
+        val dataSource = getDataSourceByPath(sourcePath)
         if (dataSource != null) {
             val duration = dataSource.durationUs
             val locatedTimestampUs = SingleThumbnailRequest(positionUs).locate(duration)[0]
             val stub =
-                stubs.find { it.request.sourceId() == source && it.positionUs == locatedTimestampUs }
+                stubs.find { it.request.sourceId() == sourceId && it.positionUs == locatedTimestampUs }
             if (stub != null) {
                 log.i("removePosition Match: $positionUs :$stubs")
                 stubs.remove(stub)
@@ -337,7 +337,7 @@ class DefaultThumbnailsEngine(
     }
 
 
-    override fun getDataSourceForId(source: String): DataSource? {
+    override fun getDataSourceByPath(source: String): DataSource? {
         return dataSources[TrackType.VIDEO].firstOrNull { it.mediaId() == source }
     }
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index 543aad32..d9516754 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -22,9 +22,9 @@ abstract class ThumbnailsEngine {
 
     abstract suspend fun queueThumbnails(list: List<ThumbnailRequest>)
 
-    abstract suspend fun removePosition(source: String, positionUs: Long)
+    abstract suspend fun removePosition(sourcePath: String, sourceId: String, positionUs: Long)
 
-    abstract fun getDataSourceForId(source: String): DataSource?
+    abstract fun getDataSourceByPath(source: String): DataSource?
 
     abstract fun cleanup()
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt
index 89a669ff..11b2f208 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt
@@ -5,6 +5,8 @@ interface ThumbnailRequest {
 
     fun threshold(): Long = 0
 
+    fun sourcePath() : String = ""
+
     fun sourceId() : String = ""
     // Could make it so that if locate() is empty, accept is called for each frame (no seeking).
     // But this only makes sense if accept signature has more information (segment, ...), and

From ae5768fbb31c856e25a8924e50e745e0ea4d8da0 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Thu, 4 Aug 2022 17:19:04 +0530
Subject: [PATCH 076/101] enable seeking if all requests for a source are
 removed (#37)

---
 .../transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt    | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index bcc74493..31ae300c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -316,6 +316,7 @@ class DefaultThumbnailsEngine(
             stubs.removeAll{
                 it.request.sourceId() == sourceId
             }
+            shouldSeek = true
             return
         }
         if (stubs.firstOrNull()?.request?.sourceId() == sourceId && positionUs == stubs.firstOrNull()?.positionUs) {

From c70439efd59ca41ef9263ea8d82ce98e5d7b5bd4 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Fri, 5 Aug 2022 13:49:51 +0530
Subject: [PATCH 077/101] log configure and start fail states of decoder and re
 throw exception (#38)

* log configure and start fail states of decoder and re throw exception

* debug build check

* decoder init log input format as well

* also log decoder's supported range
---
 build.gradle.kts                              |   4 +-
 .../transcoder/internal/codec/Decoder.kt      | 106 +++++++++++++-----
 2 files changed, 82 insertions(+), 28 deletions(-)

diff --git a/build.gradle.kts b/build.gradle.kts
index c3f98fac..794cc83b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,6 +1,6 @@
 buildscript {
 
-    extra["minSdkVersion"] = 18
+    extra["minSdkVersion"] = 21
     extra["compileSdkVersion"] = 30
     extra["targetSdkVersion"] = 30
 
@@ -12,7 +12,7 @@ buildscript {
 
     dependencies {
         classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31")
-        classpath("com.android.tools.build:gradle:4.1.2")
+        classpath("com.android.tools.build:gradle:4.1.3")
         classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.4.30") // publisher uses 1.4.20 which goes OOM
         classpath("io.deepmedia.tools:publisher:0.5.0")
     }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index 47609166..f7391cde 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -7,6 +7,7 @@ import android.media.MediaCodecList
 import android.media.MediaFormat
 import android.os.Build
 import android.view.Surface
+import com.otaliastudios.transcoder.BuildConfig
 import com.otaliastudios.transcoder.common.trackType
 import com.otaliastudios.transcoder.internal.data.ReaderChannel
 import com.otaliastudios.transcoder.internal.data.ReaderData
@@ -80,32 +81,31 @@ class Decoder(
 
     private fun createDecoderByType(format: MediaFormat, useSoftware: Boolean = false): MediaCodec {
         val mime = format.getString(MediaFormat.KEY_MIME)!!
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-            val allCodecs = MediaCodecList(MediaCodecList.ALL_CODECS)
-
-            var codecName: String? = null
-            for (info in allCodecs.codecInfos) {
-                if (info.isEncoder || (info.isHardwareAcceleratedCompat() && useSoftware)) {
-                    // log.e("Rejecting codec: ${info.name}")
-                    continue
-                }
-                try {
-                    val caps = info.getCapabilitiesForType(mime)
-                    if (caps != null && caps.isFormatSupported(format)) {
-                        codecName = info.name
-                        break
-                    } else {
-                        // log.e("Rejecting decoder: ${info.name}")
-                    }
-                } catch (e: IllegalArgumentException) {
-                    log.e("Unsupported codec type: $mime")
-                }
+
+        val allCodecs = MediaCodecList(MediaCodecList.ALL_CODECS)
+
+        var codecName: String? = null
+        for (info in allCodecs.codecInfos) {
+            if (info.isEncoder || (info.isHardwareAcceleratedCompat() && useSoftware)) {
+                // log.e("Rejecting codec: ${info.name}")
+                continue
             }
-            log.i("Using codec: $codecName for format: $format")
-            if (codecName != null) {
-                return createByCodecName(codecName)
+            try {
+                val caps = info.getCapabilitiesForType(mime)
+                if (caps != null && caps.isFormatSupported(format)) {
+                    codecName = info.name
+                    break
+                } else {
+                    // log.e("Rejecting decoder: ${info.name}")
+                }
+            } catch (e: IllegalArgumentException) {
+                log.e("Unsupported codec type: $mime")
             }
         }
+        log.i("Using codec: $codecName for format: $format")
+        if (codecName != null) {
+            return createByCodecName(codecName)
+        }
         return createDecoderByType(mime)
     }
 
@@ -121,13 +121,67 @@ class Decoder(
 
     override fun initialize(next: DecoderChannel) {
         super.initialize(next)
-        log.i("initialize()")
+        val videoCapabilities = codec.codecInfo.getCapabilitiesForType(
+            format.getString(
+                MediaFormat.KEY_MIME
+            )!!
+        ).videoCapabilities
+        log.i(
+            "initialize(): ${codec.name}, for format $format, " +
+                    "supportedHeightRange ${videoCapabilities.supportedHeights} " +
+                    "supportedWidthRange ${videoCapabilities.supportedWidths}"
+        )
         val surface = next.handleSourceFormat(format)
-        codec.configure(format, surface, null, 0)
-        codec.start()
+        try {
+            codec.configure(format, surface, null, 0)
+        }
+        catch (e: Exception) {
+            if(BuildConfig.DEBUG) {
+                log.e("Failed while configuring codec ${codec.name} for format $format")
+                logCodecException(e)
+            }
+            throw e
+        }
+        try {
+            codec.start()
+        }
+        catch (e: Exception) {
+            if(BuildConfig.DEBUG) {
+                log.e("Failed while starting codec ${codec.name} for format $format")
+                logCodecException(e)
+            }
+            throw e
+        }
         decoderReady = false
     }
 
+    private fun logCodecException(e: Exception) {
+        when(e) {
+            is CodecException -> {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                    log.e("CodecException: " +
+                            "diagnosticInfo: ${e.diagnosticInfo}, " +
+                            "isRecoverable: ${e.isRecoverable}, " +
+                            "isTransient: ${e.isTransient}, " +
+                            "errorCode:${e.errorCode}")
+                }
+                else {
+                    log.e("CodecException: " +
+                            "diagnosticInfo: ${e.diagnosticInfo}, " +
+                            "isRecoverable: ${e.isRecoverable}, " +
+                            "isTransient: ${e.isTransient}")
+
+                }
+            }
+            is java.lang.IllegalArgumentException -> {
+                log.e("IllegalArgumentException: ${e.message}")
+            }
+            is java.lang.IllegalStateException -> {
+                log.e("IllegalStateException: ${e.message}")
+            }
+        }
+    }
+
     override fun buffer(): Pair<ByteBuffer, Int>? {
         if (shouldFlush?.invoke() == true && decoderReady) {
             log.i("codec flush")

From 7cd2cf96f7639ea20dfd4aa70a40127777a90549 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Fri, 5 Aug 2022 16:21:41 +0530
Subject: [PATCH 078/101] audio decoder init crash fix (#39)

---
 .../transcoder/internal/codec/Decoder.kt             | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index f7391cde..32011336 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -126,11 +126,13 @@ class Decoder(
                 MediaFormat.KEY_MIME
             )!!
         ).videoCapabilities
-        log.i(
-            "initialize(): ${codec.name}, for format $format, " +
-                    "supportedHeightRange ${videoCapabilities.supportedHeights} " +
-                    "supportedWidthRange ${videoCapabilities.supportedWidths}"
-        )
+        if (videoCapabilities != null) {
+            log.i(
+                "initialize(): ${codec.name}, for format $format, " +
+                        "supportedHeightRange ${videoCapabilities.supportedHeights} " +
+                        "supportedWidthRange ${videoCapabilities.supportedWidths}"
+            )
+        }
         val surface = next.handleSourceFormat(format)
         try {
             codec.configure(format, surface, null, 0)

From 607b61d6d21fb0b9aaff05feab6838d9f225d7d3 Mon Sep 17 00:00:00 2001
From: Sahil Bajaj <sahilbajaj.nc@gmail.com>
Date: Wed, 10 Aug 2022 11:58:32 +0530
Subject: [PATCH 079/101] Feature/fix thumbnailer (#40)

* Retry codec creation and initialization

* Avoid removing active stubs even when removing all stubs during cleanup

Co-authored-by: Sahil Bajaj <bajaj@192.168.1.10>
---
 .../transcoder/internal/codec/Decoder.kt      | 24 +++++++++++++++----
 .../thumbnails/DefaultThumbnailsEngine.kt     | 11 ++++++---
 .../internal/thumbnails/ThumbnailsEngine.kt   |  2 +-
 3 files changed, 28 insertions(+), 9 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index 32011336..6fde5b04 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -35,7 +35,7 @@ interface DecoderChannel : Channel {
 class Decoder(
     private val format: MediaFormat, // source.getTrackFormat(track)
     continuous: Boolean, // relevant if the source sends no-render chunks. should we compensate or not?
-    useSwFor4K: Boolean = false,
+    private val useSwFor4K: Boolean = false,
     val shouldFlush: (() -> Boolean)? = null
 ) : QueuedStep<ReaderData, ReaderChannel, DecoderData, DecoderChannel>(), ReaderChannel {
 
@@ -45,10 +45,12 @@ class Decoder(
         private const val VERBOSE = false
     }
 
+    private var retry: Boolean = true
+
     private val log = Logger("Decoder(${format.trackType},${ID[format.trackType].getAndIncrement()})")
     override val channel = this
 
-    private val codec = createDecoderByType(format, useSwFor4K && format.is4K())
+    private var codec = createDecoderByType(format, useSwFor4K && format.is4K())
 
     private var decoderReady = false
 
@@ -103,8 +105,14 @@ class Decoder(
             }
         }
         log.i("Using codec: $codecName for format: $format")
-        if (codecName != null) {
-            return createByCodecName(codecName)
+        try {
+            if (codecName != null) {
+                return createByCodecName(codecName)
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+            log.e("Exception while creating codec by name: $codecName :: ${e.message}")
+            return createDecoderByType(mime)
         }
         return createDecoderByType(mime)
     }
@@ -142,7 +150,13 @@ class Decoder(
                 log.e("Failed while configuring codec ${codec.name} for format $format")
                 logCodecException(e)
             }
-            throw e
+            if (retry) {
+                retry = false
+                val mime = format.getString(MediaFormat.KEY_MIME)!!
+                codec = createDecoderByType(mime)
+                initialize(next)
+                return
+            }
         }
         try {
             codec.start()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 31ae300c..c5b6561b 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -311,15 +311,20 @@ class DefaultThumbnailsEngine(
         segments.release()
     }
 
-    override suspend fun removePosition(sourcePath: String, sourceId: String, positionUs: Long) {
+    override fun removePosition(sourcePath: String, sourceId: String, positionUs: Long) {
         if (positionUs < 0) {
-            stubs.removeAll{
+            val activeStub = stubs.firstOrNull()?.takeIf { it.request.sourceId() == sourceId }
+            stubs.removeAll {
                 it.request.sourceId() == sourceId
             }
+            if (activeStub != null) {
+                stubs.addFirst(activeStub)
+            }
             shouldSeek = true
             return
         }
-        if (stubs.firstOrNull()?.request?.sourceId() == sourceId && positionUs == stubs.firstOrNull()?.positionUs) {
+        val isStubActive = stubs.firstOrNull()?.request?.sourceId() == sourceId && positionUs == stubs.firstOrNull()?.positionUs
+        if (isStubActive) {
             return
         }
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index d9516754..c672138c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -22,7 +22,7 @@ abstract class ThumbnailsEngine {
 
     abstract suspend fun queueThumbnails(list: List<ThumbnailRequest>)
 
-    abstract suspend fun removePosition(sourcePath: String, sourceId: String, positionUs: Long)
+    abstract fun removePosition(sourcePath: String, sourceId: String, positionUs: Long)
 
     abstract fun getDataSourceByPath(source: String): DataSource?
 

From 7c5fcdb7b52d775f630b0cb58bda41b730d3faed Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Wed, 10 Aug 2022 15:37:32 +0530
Subject: [PATCH 080/101] codec name cannot be fetched before its configured
 (#41)

---
 .../java/com/otaliastudios/transcoder/internal/codec/Decoder.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index 6fde5b04..b49cab36 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -136,7 +136,7 @@ class Decoder(
         ).videoCapabilities
         if (videoCapabilities != null) {
             log.i(
-                "initialize(): ${codec.name}, for format $format, " +
+                "initialize(): ${codec.codecInfo.name}, for format $format, " +
                         "supportedHeightRange ${videoCapabilities.supportedHeights} " +
                         "supportedWidthRange ${videoCapabilities.supportedWidths}"
             )

From 53ecb8577947290753e45ec8fb9cb547651e284e Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Thu, 11 Aug 2022 17:24:18 +0530
Subject: [PATCH 081/101] destroy segments and release decoders if no more
 pending requests (#42)

---
 .../com/otaliastudios/transcoder/internal/CustomSegments.kt  | 2 ++
 .../internal/thumbnails/DefaultThumbnailsEngine.kt           | 5 ++++-
 .../transcoder/internal/thumbnails/ThumbnailsEngine.kt       | 3 +++
 3 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
index de69802d..f843902b 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
@@ -102,6 +102,8 @@ class CustomSegments(
                 segmentMap.remove(it)
             }
             if(releaseAll) {
+                currentSegment = null
+                currentSegmentMapKey = null
                 segmentMap.clear()
             }
         }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index c5b6561b..b8f6992a 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -297,7 +297,10 @@ class DefaultThumbnailsEngine(
                 // avoid calling hasNext if we advanced.
                 val completed = !advanced && !segments.hasNext()
                 if (completed || stubs.isEmpty()) {
-                    log.i("loop broken $stubs")
+                    log.i("loop broken $stubs $hasMoreRequestsIncoming")
+                    if (!hasMoreRequestsIncoming) {
+                        segments.release()
+                    }
                     break
                 } else if (!advanced) {
                     delay(WAIT_MS)
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index c672138c..e1cc30ad 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -28,6 +28,9 @@ abstract class ThumbnailsEngine {
 
     abstract fun cleanup()
 
+    @Volatile
+    var hasMoreRequestsIncoming = true
+
     companion object {
         private val log = Logger("ThumbnailsEngine")
 

From d94d2db58968ab839009032b937e6b1e9fff93e3 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Wed, 17 Aug 2022 14:15:37 +0530
Subject: [PATCH 082/101] when sample time is -1 and keyframetimestamps don't
 exist, seek to 0 instead (#43)

---
 .../otaliastudios/transcoder/source/DefaultDataSource.java  | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index 3fcab518..20c5f70b 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -111,7 +111,11 @@ public long requestKeyFrameTimestamps() {
 
         if (sampleTime == -1 || (keyFrameTimestamps.size() > 0 && sampleTime == keyFrameTimestamps.get(keyFrameTimestamps.size() - 1))) {
             lastKeyFrame = true;
-            mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + SEEK_THRESHOLD, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+            if (!keyFrameTimestamps.isEmpty()) {
+                mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + SEEK_THRESHOLD, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+            } else {
+                mExtractor.seekTo(0L, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+            }
             return -1;
         }
 //        LOG.i("keyFrameStartTime:" + sampleTime);

From 8ac290a567fa4c2481b2a96f1e3caa6dafcb27af Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Mon, 22 Aug 2022 20:11:31 +0530
Subject: [PATCH 083/101] use sonic audio processor to resample and stretch
 audio (#44)

---
 .../transcoder/internal/audio/AudioEngine.kt  |  24 +-
 .../internal/audio/SonicAudioProcessor.kt     | 531 ++++++++++++++++++
 2 files changed, 549 insertions(+), 6 deletions(-)
 create mode 100644 lib/src/main/java/com/otaliastudios/transcoder/internal/audio/SonicAudioProcessor.kt

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
index de3b2362..e2898361 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
@@ -18,8 +18,6 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.resample.AudioResampler
 import com.otaliastudios.transcoder.stretch.AudioStretcher
 import java.util.concurrent.atomic.AtomicInteger
-import kotlin.math.ceil
-import kotlin.math.floor
 
 /**
  * Performs audio rendering, from decoder output to encoder input, applying sample rate conversion,
@@ -28,7 +26,8 @@ import kotlin.math.floor
 class AudioEngine(
     private val stretcher: AudioStretcher,
     private val resampler: AudioResampler,
-    private val targetFormat: MediaFormat
+    private val targetFormat: MediaFormat,
+    private val stretch : Float = 1.0f
 ) : QueuedStep<DecoderData, DecoderChannel, EncoderData, EncoderChannel>(), DecoderChannel {
 
     companion object {
@@ -45,7 +44,7 @@ class AudioEngine(
     private lateinit var rawFormat: MediaFormat
     private lateinit var chunks: ChunkQueue
     private lateinit var remixer: AudioRemixer
-
+    private lateinit var sonicExo: SonicAudioProcessor
     override fun handleSourceFormat(sourceFormat: MediaFormat): Surface? = null
 
     override fun handleRawFormat(rawFormat: MediaFormat) {
@@ -53,14 +52,16 @@ class AudioEngine(
         this.rawFormat = rawFormat
         remixer = AudioRemixer[rawFormat.channels, targetFormat.channels]
         chunks = ChunkQueue(rawFormat.sampleRate, rawFormat.channels)
-        resampler.createStream(rawFormat.sampleRate, targetFormat.sampleRate, targetFormat.channels)
+//        resampler.createStream(rawFormat.sampleRate, targetFormat.sampleRate, targetFormat.channels)
+        sonicExo = SonicAudioProcessor(rawFormat.sampleRate, rawFormat.channels, stretch,1f, targetFormat.sampleRate)
     }
 
     override fun enqueueEos(data: DecoderData) {
         log.i("enqueueEos()")
         data.release(false)
         chunks.enqueueEos()
-        resampler.destroyStream()
+        sonicExo.queueEndOfStream()
+//        resampler.destroyStream()
     }
 
     override fun enqueue(data: DecoderData) {
@@ -84,6 +85,7 @@ class AudioEngine(
         return chunks.drain(
             eos = State.Eos(EncoderData(outBytes, outId, 0))
         ) { inBuffer, timeUs, stretch ->
+/*
             val outSize = outBuffer.remaining()
             val inSize = inBuffer.remaining()
 
@@ -119,6 +121,16 @@ class AudioEngine(
                 targetFormat.sampleRate,
                 targetFormat.channels
             )
+*/
+            sonicExo.queueInput(inBuffer)
+
+            val stretchBuffer = buffers.acquire("stretch", outBuffer.capacity())
+
+            sonicExo.getOutput(stretchBuffer)
+            stretchBuffer.flip()
+
+            // Remix
+            remixer.remix(stretchBuffer, outBuffer)
             outBuffer.flip()
 
             // Adjust position and dispatch.
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/SonicAudioProcessor.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/SonicAudioProcessor.kt
new file mode 100644
index 00000000..730ff884
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/SonicAudioProcessor.kt
@@ -0,0 +1,531 @@
+package com.otaliastudios.transcoder.internal.audio
+
+import java.nio.ShortBuffer
+import java.util.Arrays
+
+/**
+ * Sonic audio stream processor for time/pitch stretching.
+ *
+ *
+ * Based on https://github.com/waywardgeek/sonic.
+ */
+/* package */
+internal class SonicAudioProcessor(
+    private val inputSampleRateHz: Int,
+    private val channelCount: Int,
+    private val speed: Float,
+    private val pitch: Float,
+    outputSampleRateHz: Int
+) {
+    private val rate: Float
+    private val minPeriod: Int
+    private val maxPeriod: Int
+    private val maxRequiredFrameCount: Int
+    private val downSampleBuffer: ShortArray
+    private var inputBuffer: ShortArray
+    private var inputFrameCount = 0
+    private var outputBuffer: ShortArray
+    private var outputFrameCount = 0
+    private var pitchBuffer: ShortArray
+    private var pitchFrameCount = 0
+    private var oldRatePosition = 0
+    private var newRatePosition = 0
+    private var remainingInputToCopyFrameCount = 0
+    private var prevPeriod = 0
+    private var prevMinDiff = 0
+    private var minDiff = 0
+    private var maxDiff = 0
+
+    /**
+     * Creates a new Sonic audio stream processor.
+     *
+     * @param inputSampleRateHz The sample rate of input audio, in hertz.
+     * @param channelCount The number of channels in the input audio.
+     * @param speed The speedup factor for output audio.
+     * @param pitch The pitch factor for output audio.
+     * @param outputSampleRateHz The sample rate for output audio, in hertz.
+     */
+    init {
+        rate = inputSampleRateHz.toFloat() / outputSampleRateHz
+        minPeriod = inputSampleRateHz / MAXIMUM_PITCH
+        maxPeriod = inputSampleRateHz / MINIMUM_PITCH
+        maxRequiredFrameCount = 2 * maxPeriod
+        downSampleBuffer = ShortArray(maxRequiredFrameCount)
+        inputBuffer = ShortArray(maxRequiredFrameCount * channelCount)
+        outputBuffer = ShortArray(maxRequiredFrameCount * channelCount)
+        pitchBuffer = ShortArray(maxRequiredFrameCount * channelCount)
+    }
+
+    /**
+     * Returns the number of bytes that have been input, but will not be processed until more input
+     * data is provided.
+     */
+    val pendingInputBytes: Int
+        get() = inputFrameCount * channelCount * BYTES_PER_SAMPLE
+
+    /**
+     * Queues remaining data from `buffer`, and advances its position by the number of bytes
+     * consumed.
+     *
+     * @param buffer A [ShortBuffer] containing input data between its position and limit.
+     */
+    fun queueInput(buffer: ShortBuffer) {
+        val framesToWrite = buffer.remaining() / channelCount
+        val bytesToWrite = framesToWrite * channelCount * 2
+        inputBuffer = ensureSpaceForAdditionalFrames(inputBuffer, inputFrameCount, framesToWrite)
+        buffer[inputBuffer, inputFrameCount * channelCount, bytesToWrite / 2]
+        inputFrameCount += framesToWrite
+        processStreamInput()
+    }
+
+    /**
+     * Gets available output, outputting to the start of `buffer`. The buffer's position will be
+     * advanced by the number of bytes written.
+     *
+     * @param buffer A [ShortBuffer] into which output will be written.
+     */
+    fun getOutput(buffer: ShortBuffer) {
+        val framesToRead = Math.min(buffer.remaining() / channelCount, outputFrameCount)
+        buffer.put(outputBuffer, 0, framesToRead * channelCount)
+        outputFrameCount -= framesToRead
+        System.arraycopy(
+            outputBuffer,
+            framesToRead * channelCount,
+            outputBuffer,
+            0,
+            outputFrameCount * channelCount
+        )
+    }
+
+    /**
+     * Forces generating output using whatever data has been queued already. No extra delay will be
+     * added to the output, but flushing in the middle of words could introduce distortion.
+     */
+    fun queueEndOfStream() {
+        val remainingFrameCount = inputFrameCount
+        val s = speed / pitch
+        val r = rate * pitch
+        val expectedOutputFrames =
+            outputFrameCount + ((remainingFrameCount / s + pitchFrameCount) / r + 0.5f).toInt()
+
+        // Add enough silence to flush both input and pitch buffers.
+        inputBuffer = ensureSpaceForAdditionalFrames(
+            inputBuffer, inputFrameCount, remainingFrameCount + 2 * maxRequiredFrameCount
+        )
+        for (xSample in 0 until 2 * maxRequiredFrameCount * channelCount) {
+            inputBuffer[remainingFrameCount * channelCount + xSample] = 0
+        }
+        inputFrameCount += 2 * maxRequiredFrameCount
+        processStreamInput()
+        // Throw away any extra frames we generated due to the silence we added.
+        if (outputFrameCount > expectedOutputFrames) {
+            outputFrameCount = expectedOutputFrames
+        }
+        // Empty input and pitch buffers.
+        inputFrameCount = 0
+        remainingInputToCopyFrameCount = 0
+        pitchFrameCount = 0
+    }
+
+    /** Clears state in preparation for receiving a new stream of input buffers.  */
+    fun flush() {
+        inputFrameCount = 0
+        outputFrameCount = 0
+        pitchFrameCount = 0
+        oldRatePosition = 0
+        newRatePosition = 0
+        remainingInputToCopyFrameCount = 0
+        prevPeriod = 0
+        prevMinDiff = 0
+        minDiff = 0
+        maxDiff = 0
+    }
+
+    /** Returns the size of output that can be read with [.getOutput], in bytes.  */
+    val outputSize: Int
+        get() = outputFrameCount * channelCount * BYTES_PER_SAMPLE
+    // Internal methods.
+    /**
+     * Returns `buffer` or a copy of it, such that there is enough space in the returned buffer
+     * to store `newFrameCount` additional frames.
+     *
+     * @param buffer The buffer.
+     * @param frameCount The number of frames already in the buffer.
+     * @param additionalFrameCount The number of additional frames that need to be stored in the
+     * buffer.
+     * @return A buffer with enough space for the additional frames.
+     */
+    private fun ensureSpaceForAdditionalFrames(
+        buffer: ShortArray, frameCount: Int, additionalFrameCount: Int
+    ): ShortArray {
+        val currentCapacityFrames = buffer.size / channelCount
+        return if (frameCount + additionalFrameCount <= currentCapacityFrames) {
+            buffer
+        } else {
+            val newCapacityFrames = 3 * currentCapacityFrames / 2 + additionalFrameCount
+            Arrays.copyOf(buffer, newCapacityFrames * channelCount)
+        }
+    }
+
+    private fun removeProcessedInputFrames(positionFrames: Int) {
+        val remainingFrames = inputFrameCount - positionFrames
+        System.arraycopy(
+            inputBuffer,
+            positionFrames * channelCount,
+            inputBuffer,
+            0,
+            remainingFrames * channelCount
+        )
+        inputFrameCount = remainingFrames
+    }
+
+    private fun copyToOutput(samples: ShortArray, positionFrames: Int, frameCount: Int) {
+        outputBuffer = ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, frameCount)
+        System.arraycopy(
+            samples,
+            positionFrames * channelCount,
+            outputBuffer,
+            outputFrameCount * channelCount,
+            frameCount * channelCount
+        )
+        outputFrameCount += frameCount
+    }
+
+    private fun copyInputToOutput(positionFrames: Int): Int {
+        val frameCount = Math.min(maxRequiredFrameCount, remainingInputToCopyFrameCount)
+        copyToOutput(inputBuffer, positionFrames, frameCount)
+        remainingInputToCopyFrameCount -= frameCount
+        return frameCount
+    }
+
+    private fun downSampleInput(samples: ShortArray, position: Int, skip: Int) {
+        // If skip is greater than one, average skip samples together and write them to the down-sample
+        // buffer. If channelCount is greater than one, mix the channels together as we down sample.
+        var position = position
+        val frameCount = maxRequiredFrameCount / skip
+        val samplesPerValue = channelCount * skip
+        position *= channelCount
+        for (i in 0 until frameCount) {
+            var value = 0
+            for (j in 0 until samplesPerValue) {
+                value += samples[position + i * samplesPerValue + j].toInt()
+            }
+            value /= samplesPerValue
+            downSampleBuffer[i] = value.toShort()
+        }
+    }
+
+    private fun findPitchPeriodInRange(
+        samples: ShortArray,
+        position: Int,
+        minPeriod: Int,
+        maxPeriod: Int
+    ): Int {
+        // Find the best frequency match in the range, and given a sample skip multiple. For now, just
+        // find the pitch of the first channel.
+        var position = position
+        var bestPeriod = 0
+        var worstPeriod = 255
+        var minDiff = 1
+        var maxDiff = 0
+        position *= channelCount
+        for (period in minPeriod..maxPeriod) {
+            var diff = 0
+            for (i in 0 until period) {
+                val sVal = samples[position + i]
+                val pVal = samples[position + period + i]
+                diff += Math.abs(sVal - pVal)
+            }
+            // Note that the highest number of samples we add into diff will be less than 256, since we
+            // skip samples. Thus, diff is a 24 bit number, and we can safely multiply by numSamples
+            // without overflow.
+            if (diff * bestPeriod < minDiff * period) {
+                minDiff = diff
+                bestPeriod = period
+            }
+            if (diff * worstPeriod > maxDiff * period) {
+                maxDiff = diff
+                worstPeriod = period
+            }
+        }
+        this.minDiff = minDiff / bestPeriod
+        this.maxDiff = maxDiff / worstPeriod
+        return bestPeriod
+    }
+
+    /**
+     * Returns whether the previous pitch period estimate is a better approximation, which can occur
+     * at the abrupt end of voiced words.
+     */
+    private fun previousPeriodBetter(minDiff: Int, maxDiff: Int): Boolean {
+        if (minDiff == 0 || prevPeriod == 0) {
+            return false
+        }
+        if (maxDiff > minDiff * 3) {
+            // Got a reasonable match this period.
+            return false
+        }
+        return if (minDiff * 2 <= prevMinDiff * 3) {
+            // Mismatch is not that much greater this period.
+            false
+        } else true
+    }
+
+    private fun findPitchPeriod(samples: ShortArray, position: Int): Int {
+        // Find the pitch period. This is a critical step, and we may have to try multiple ways to get a
+        // good answer. This version uses AMDF. To improve speed, we down sample by an integer factor
+        // get in the 11 kHz range, and then do it again with a narrower frequency range without down
+        // sampling.
+        var period: Int
+        val retPeriod: Int
+        val skip = if (inputSampleRateHz > AMDF_FREQUENCY) inputSampleRateHz / AMDF_FREQUENCY else 1
+        if (channelCount == 1 && skip == 1) {
+            period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod)
+        } else {
+            downSampleInput(samples, position, skip)
+            period = findPitchPeriodInRange(downSampleBuffer, 0, minPeriod / skip, maxPeriod / skip)
+            if (skip != 1) {
+                period *= skip
+                var minP = period - skip * 4
+                var maxP = period + skip * 4
+                if (minP < minPeriod) {
+                    minP = minPeriod
+                }
+                if (maxP > maxPeriod) {
+                    maxP = maxPeriod
+                }
+                period = if (channelCount == 1) {
+                    findPitchPeriodInRange(samples, position, minP, maxP)
+                } else {
+                    downSampleInput(samples, position, 1)
+                    findPitchPeriodInRange(downSampleBuffer, 0, minP, maxP)
+                }
+            }
+        }
+        retPeriod = if (previousPeriodBetter(minDiff, maxDiff)) {
+            prevPeriod
+        } else {
+            period
+        }
+        prevMinDiff = minDiff
+        prevPeriod = period
+        return retPeriod
+    }
+
+    private fun moveNewSamplesToPitchBuffer(originalOutputFrameCount: Int) {
+        val frameCount = outputFrameCount - originalOutputFrameCount
+        pitchBuffer = ensureSpaceForAdditionalFrames(pitchBuffer, pitchFrameCount, frameCount)
+        System.arraycopy(
+            outputBuffer,
+            originalOutputFrameCount * channelCount,
+            pitchBuffer,
+            pitchFrameCount * channelCount,
+            frameCount * channelCount
+        )
+        outputFrameCount = originalOutputFrameCount
+        pitchFrameCount += frameCount
+    }
+
+    private fun removePitchFrames(frameCount: Int) {
+        if (frameCount == 0) {
+            return
+        }
+        System.arraycopy(
+            pitchBuffer,
+            frameCount * channelCount,
+            pitchBuffer,
+            0,
+            (pitchFrameCount - frameCount) * channelCount
+        )
+        pitchFrameCount -= frameCount
+    }
+
+    private fun interpolate(
+        `in`: ShortArray,
+        inPos: Int,
+        oldSampleRate: Int,
+        newSampleRate: Int
+    ): Short {
+        val left = `in`[inPos]
+        val right = `in`[inPos + channelCount]
+        val position = newRatePosition * oldSampleRate
+        val leftPosition = oldRatePosition * newSampleRate
+        val rightPosition = (oldRatePosition + 1) * newSampleRate
+        val ratio = rightPosition - position
+        val width = rightPosition - leftPosition
+        return ((ratio * left + (width - ratio) * right) / width).toShort()
+    }
+
+    private fun adjustRate(rate: Float, originalOutputFrameCount: Int) {
+        if (outputFrameCount == originalOutputFrameCount) {
+            return
+        }
+        var newSampleRate = (inputSampleRateHz / rate).toInt()
+        var oldSampleRate = inputSampleRateHz
+        // Set these values to help with the integer math.
+        while (newSampleRate > 1 shl 14 || oldSampleRate > 1 shl 14) {
+            newSampleRate /= 2
+            oldSampleRate /= 2
+        }
+        moveNewSamplesToPitchBuffer(originalOutputFrameCount)
+        // Leave at least one pitch sample in the buffer.
+        for (position in 0 until pitchFrameCount - 1) {
+            while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) {
+                outputBuffer = ensureSpaceForAdditionalFrames(
+                    outputBuffer, outputFrameCount,  /* additionalFrameCount= */1
+                )
+                for (i in 0 until channelCount) {
+                    outputBuffer[outputFrameCount * channelCount + i] = interpolate(
+                        pitchBuffer,
+                        position * channelCount + i,
+                        oldSampleRate,
+                        newSampleRate
+                    )
+                }
+                newRatePosition++
+                outputFrameCount++
+            }
+            oldRatePosition++
+            if (oldRatePosition == oldSampleRate) {
+                oldRatePosition = 0
+                //        Assertions.checkState(newRatePosition == newSampleRate);
+                newRatePosition = 0
+            }
+        }
+        removePitchFrames(pitchFrameCount - 1)
+    }
+
+    private fun skipPitchPeriod(
+        samples: ShortArray,
+        position: Int,
+        speed: Float,
+        period: Int
+    ): Int {
+        // Skip over a pitch period, and copy period/speed samples to the output.
+        val newFrameCount: Int
+        if (speed >= 2.0f) {
+            newFrameCount = (period / (speed - 1.0f)).toInt()
+        } else {
+            newFrameCount = period
+            remainingInputToCopyFrameCount = (period * (2.0f - speed) / (speed - 1.0f)).toInt()
+        }
+        outputBuffer = ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, newFrameCount)
+        overlapAdd(
+            newFrameCount,
+            channelCount,
+            outputBuffer,
+            outputFrameCount,
+            samples,
+            position,
+            samples,
+            position + period
+        )
+        outputFrameCount += newFrameCount
+        return newFrameCount
+    }
+
+    private fun insertPitchPeriod(
+        samples: ShortArray,
+        position: Int,
+        speed: Float,
+        period: Int
+    ): Int {
+        // Insert a pitch period, and determine how much input to copy directly.
+        val newFrameCount: Int
+        if (speed < 0.5f) {
+            newFrameCount = (period * speed / (1.0f - speed)).toInt()
+        } else {
+            newFrameCount = period
+            remainingInputToCopyFrameCount =
+                (period * (2.0f * speed - 1.0f) / (1.0f - speed)).toInt()
+        }
+        outputBuffer =
+            ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, period + newFrameCount)
+        System.arraycopy(
+            samples,
+            position * channelCount,
+            outputBuffer,
+            outputFrameCount * channelCount,
+            period * channelCount
+        )
+        overlapAdd(
+            newFrameCount,
+            channelCount,
+            outputBuffer,
+            outputFrameCount + period,
+            samples,
+            position + period,
+            samples,
+            position
+        )
+        outputFrameCount += period + newFrameCount
+        return newFrameCount
+    }
+
+    private fun changeSpeed(speed: Float) {
+        if (inputFrameCount < maxRequiredFrameCount) {
+            return
+        }
+        val frameCount = inputFrameCount
+        var positionFrames = 0
+        do {
+            positionFrames += if (remainingInputToCopyFrameCount > 0) {
+                copyInputToOutput(positionFrames)
+            } else {
+                val period = findPitchPeriod(inputBuffer, positionFrames)
+                if (speed > 1.0) {
+                    period + skipPitchPeriod(inputBuffer, positionFrames, speed, period)
+                } else {
+                    insertPitchPeriod(inputBuffer, positionFrames, speed, period)
+                }
+            }
+        } while (positionFrames + maxRequiredFrameCount <= frameCount)
+        removeProcessedInputFrames(positionFrames)
+    }
+
+    private fun processStreamInput() {
+        // Resample as many pitch periods as we have buffered on the input.
+        val originalOutputFrameCount = outputFrameCount
+        val s = speed / pitch
+        val r = rate * pitch
+        if (s > 1.00001 || s < 0.99999) {
+            changeSpeed(s)
+        } else {
+            copyToOutput(inputBuffer, 0, inputFrameCount)
+            inputFrameCount = 0
+        }
+        if (r != 1.0f) {
+            adjustRate(r, originalOutputFrameCount)
+        }
+    }
+
+    companion object {
+        private const val MINIMUM_PITCH = 65
+        private const val MAXIMUM_PITCH = 400
+        private const val AMDF_FREQUENCY = 4000
+        private const val BYTES_PER_SAMPLE = 2
+        private fun overlapAdd(
+            frameCount: Int,
+            channelCount: Int,
+            out: ShortArray,
+            outPosition: Int,
+            rampDown: ShortArray,
+            rampDownPosition: Int,
+            rampUp: ShortArray,
+            rampUpPosition: Int
+        ) {
+            for (i in 0 until channelCount) {
+                var o = outPosition * channelCount + i
+                var u = rampUpPosition * channelCount + i
+                var d = rampDownPosition * channelCount + i
+                for (t in 0 until frameCount) {
+                    out[o] =
+                        ((rampDown[d] * (frameCount - t) + rampUp[u] * t) / frameCount).toShort()
+                    o += channelCount
+                    d += channelCount
+                    u += channelCount
+                }
+            }
+        }
+    }
+}
\ No newline at end of file

From 00f12e7c58934a6bb8a8f16e49d674fdafcb614e Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Wed, 24 Aug 2022 11:57:07 +0530
Subject: [PATCH 084/101] sonic audio engine (#45)

---
 .../transcoder/internal/audio/AudioEngine.kt  |  24 +---
 .../internal/audio/SonicAudioEngine.kt        | 103 ++++++++++++++++++
 2 files changed, 109 insertions(+), 18 deletions(-)
 create mode 100644 lib/src/main/java/com/otaliastudios/transcoder/internal/audio/SonicAudioEngine.kt

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
index e2898361..de3b2362 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/AudioEngine.kt
@@ -18,6 +18,8 @@ import com.otaliastudios.transcoder.internal.utils.Logger
 import com.otaliastudios.transcoder.resample.AudioResampler
 import com.otaliastudios.transcoder.stretch.AudioStretcher
 import java.util.concurrent.atomic.AtomicInteger
+import kotlin.math.ceil
+import kotlin.math.floor
 
 /**
  * Performs audio rendering, from decoder output to encoder input, applying sample rate conversion,
@@ -26,8 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger
 class AudioEngine(
     private val stretcher: AudioStretcher,
     private val resampler: AudioResampler,
-    private val targetFormat: MediaFormat,
-    private val stretch : Float = 1.0f
+    private val targetFormat: MediaFormat
 ) : QueuedStep<DecoderData, DecoderChannel, EncoderData, EncoderChannel>(), DecoderChannel {
 
     companion object {
@@ -44,7 +45,7 @@ class AudioEngine(
     private lateinit var rawFormat: MediaFormat
     private lateinit var chunks: ChunkQueue
     private lateinit var remixer: AudioRemixer
-    private lateinit var sonicExo: SonicAudioProcessor
+
     override fun handleSourceFormat(sourceFormat: MediaFormat): Surface? = null
 
     override fun handleRawFormat(rawFormat: MediaFormat) {
@@ -52,16 +53,14 @@ class AudioEngine(
         this.rawFormat = rawFormat
         remixer = AudioRemixer[rawFormat.channels, targetFormat.channels]
         chunks = ChunkQueue(rawFormat.sampleRate, rawFormat.channels)
-//        resampler.createStream(rawFormat.sampleRate, targetFormat.sampleRate, targetFormat.channels)
-        sonicExo = SonicAudioProcessor(rawFormat.sampleRate, rawFormat.channels, stretch,1f, targetFormat.sampleRate)
+        resampler.createStream(rawFormat.sampleRate, targetFormat.sampleRate, targetFormat.channels)
     }
 
     override fun enqueueEos(data: DecoderData) {
         log.i("enqueueEos()")
         data.release(false)
         chunks.enqueueEos()
-        sonicExo.queueEndOfStream()
-//        resampler.destroyStream()
+        resampler.destroyStream()
     }
 
     override fun enqueue(data: DecoderData) {
@@ -85,7 +84,6 @@ class AudioEngine(
         return chunks.drain(
             eos = State.Eos(EncoderData(outBytes, outId, 0))
         ) { inBuffer, timeUs, stretch ->
-/*
             val outSize = outBuffer.remaining()
             val inSize = inBuffer.remaining()
 
@@ -121,16 +119,6 @@ class AudioEngine(
                 targetFormat.sampleRate,
                 targetFormat.channels
             )
-*/
-            sonicExo.queueInput(inBuffer)
-
-            val stretchBuffer = buffers.acquire("stretch", outBuffer.capacity())
-
-            sonicExo.getOutput(stretchBuffer)
-            stretchBuffer.flip()
-
-            // Remix
-            remixer.remix(stretchBuffer, outBuffer)
             outBuffer.flip()
 
             // Adjust position and dispatch.
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/SonicAudioEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/SonicAudioEngine.kt
new file mode 100644
index 00000000..43bbff51
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/SonicAudioEngine.kt
@@ -0,0 +1,103 @@
+@file:Suppress("ReturnCount")
+
+package com.otaliastudios.transcoder.internal.audio
+
+import android.media.MediaFormat
+import android.media.MediaFormat.KEY_CHANNEL_COUNT
+import android.media.MediaFormat.KEY_SAMPLE_RATE
+import android.view.Surface
+import com.otaliastudios.transcoder.internal.audio.remix.AudioRemixer
+import com.otaliastudios.transcoder.internal.codec.DecoderChannel
+import com.otaliastudios.transcoder.internal.codec.DecoderData
+import com.otaliastudios.transcoder.internal.codec.DecoderTimerData
+import com.otaliastudios.transcoder.internal.codec.EncoderChannel
+import com.otaliastudios.transcoder.internal.codec.EncoderData
+import com.otaliastudios.transcoder.internal.pipeline.QueuedStep
+import com.otaliastudios.transcoder.internal.pipeline.State
+import com.otaliastudios.transcoder.internal.utils.Logger
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * Performs audio rendering, from decoder output to encoder input, applying sample rate conversion,
+ * remixing, stretching using sonic.
+ */
+class SonicAudioEngine(
+    private val targetFormat: MediaFormat,
+    private val stretch : Float = 1.0f
+) : QueuedStep<DecoderData, DecoderChannel, EncoderData, EncoderChannel>(), DecoderChannel {
+
+    companion object {
+        private val ID = AtomicInteger(0)
+    }
+    private val log = Logger("AudioEngine(${ID.getAndIncrement()})")
+
+    override val channel = this
+    private val buffers = ShortBuffers()
+
+    private val MediaFormat.sampleRate get() = getInteger(KEY_SAMPLE_RATE)
+    private val MediaFormat.channels get() = getInteger(KEY_CHANNEL_COUNT)
+
+    private lateinit var rawFormat: MediaFormat
+    private lateinit var chunks: ChunkQueue
+    private lateinit var remixer: AudioRemixer
+    private lateinit var sonicExo: SonicAudioProcessor
+    override fun handleSourceFormat(sourceFormat: MediaFormat): Surface? = null
+
+    override fun handleRawFormat(rawFormat: MediaFormat) {
+        log.i("handleRawFormat($rawFormat)")
+        this.rawFormat = rawFormat
+        remixer = AudioRemixer[rawFormat.channels, targetFormat.channels]
+        chunks = ChunkQueue(rawFormat.sampleRate, rawFormat.channels)
+        sonicExo = SonicAudioProcessor(rawFormat.sampleRate, rawFormat.channels, stretch,1f, targetFormat.sampleRate)
+    }
+
+    override fun enqueueEos(data: DecoderData) {
+        log.i("enqueueEos()")
+        data.release(false)
+        chunks.enqueueEos()
+        sonicExo.queueEndOfStream()
+    }
+
+    override fun enqueue(data: DecoderData) {
+        val stretch = (data as? DecoderTimerData)?.timeStretch ?: 1.0
+        log.v("Enqueue: ${data.timeUs}   $this")
+        chunks.enqueue(data.buffer.asShortBuffer(), data.timeUs, stretch) {
+            data.release(false)
+        }
+    }
+
+    override fun drain(): State<EncoderData> {
+        if (chunks.isEmpty()) {
+            log.i("drain(): no chunks, waiting...")
+            return State.Wait
+        }
+        val (outBytes, outId) = next.buffer() ?: return run {
+            log.i("drain(): no next buffer, waiting...")
+            State.Wait
+        }
+        val outBuffer = outBytes.asShortBuffer()
+        return chunks.drain(
+            eos = State.Eos(EncoderData(outBytes, outId, 0))
+        ) { inBuffer, timeUs, stretch ->
+
+            sonicExo.queueInput(inBuffer)
+
+            val stretchBuffer = buffers.acquire("stretch", outBuffer.capacity())
+
+            sonicExo.getOutput(stretchBuffer)
+            stretchBuffer.flip()
+
+            // Remix
+            remixer.remix(stretchBuffer, outBuffer)
+            outBuffer.flip()
+
+            // Adjust position and dispatch.
+            outBytes.clear()
+            outBytes.limit(outBuffer.limit() * BYTES_PER_SHORT)
+            outBytes.position(outBuffer.position() * BYTES_PER_SHORT)
+
+            log.v("Drain: ts: $timeUs  size: ${outBuffer.limit()}")
+            State.Ok(EncoderData(outBytes, outId, timeUs))
+        }
+    }
+}

From 1ef624279b1c004784cda43f0f2cb5c241682790 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Mon, 29 Aug 2022 13:59:48 +0530
Subject: [PATCH 085/101] use video track duration instead of media duration.
 put stubs empty check inside while condition (#46)

---
 .../thumbnails/DefaultThumbnailsEngine.kt     | 55 ++++++++++---------
 1 file changed, 29 insertions(+), 26 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index b8f6992a..0815057f 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -263,7 +263,7 @@ class DefaultThumbnailsEngine(
         map.forEach { entry ->
             val dataSource = getDataSourceByPath(entry.key)
             if (dataSource != null) {
-                val duration = dataSource.durationUs
+                val duration = dataSource.getVideoTrackDuration()
                 val positions = entry.value.flatMap { request ->
                     request.locate(duration).map { it to request }
                 }.sortedBy { it.first }
@@ -279,36 +279,39 @@ class DefaultThumbnailsEngine(
 
         }
 
-        if (stubs.isNotEmpty()) {
-            while (currentCoroutineContext().isActive) {
-                val segment =
-                    stubs.firstOrNull()?.request?.sourcePath()?.let { segments.getSegment(it) }
-                if (VERBOSE) {
-                    log.i("loop advancing for $segment")
-                }
-                val advanced = try {
-                    segment?.advance() ?: false
-                } catch (e: Exception) {
-                    if (e !is CancellationException) {
-                        currentCoroutineContext().ensureActive()
-                    }
-                    throw e
+        while (currentCoroutineContext().isActive && stubs.isNotEmpty()) {
+            val segment =
+                stubs.firstOrNull()?.request?.sourcePath()?.let { segments.getSegment(it) }
+            if (VERBOSE) {
+                log.i("loop advancing for $segment")
+            }
+            val advanced = try {
+                segment?.advance() ?: false
+            } catch (e: Exception) {
+                if (e !is CancellationException) {
+                    currentCoroutineContext().ensureActive()
                 }
-                // avoid calling hasNext if we advanced.
-                val completed = !advanced && !segments.hasNext()
-                if (completed || stubs.isEmpty()) {
-                    log.i("loop broken $stubs $hasMoreRequestsIncoming")
-                    if (!hasMoreRequestsIncoming) {
-                        segments.release()
-                    }
-                    break
-                } else if (!advanced) {
-                    delay(WAIT_MS)
+                throw e
+            }
+            // avoid calling hasNext if we advanced.
+            val completed = !advanced && !segments.hasNext()
+            if (completed || stubs.isEmpty()) {
+                log.i("loop broken $stubs $hasMoreRequestsIncoming")
+                if (!hasMoreRequestsIncoming) {
+                    segments.release()
                 }
+                break
+            } else if (!advanced) {
+                delay(WAIT_MS)
             }
         }
+
     }
 
+    private fun DataSource.getVideoTrackDuration() =
+        getTrackFormat(TrackType.VIDEO)?.getLong(MediaFormat.KEY_DURATION)
+            ?: durationUs
+
     fun finish() {
         this.finish = true
         segments.release()
@@ -333,7 +336,7 @@ class DefaultThumbnailsEngine(
 
         val dataSource = getDataSourceByPath(sourcePath)
         if (dataSource != null) {
-            val duration = dataSource.durationUs
+            val duration = dataSource.getVideoTrackDuration()
             val locatedTimestampUs = SingleThumbnailRequest(positionUs).locate(duration)[0]
             val stub =
                 stubs.find { it.request.sourceId() == sourceId && it.positionUs == locatedTimestampUs }

From 162c808272c46b3c662bc1df916b4a6af501dda7 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Mon, 29 Aug 2022 16:38:19 +0530
Subject: [PATCH 086/101] find create and destroy segments based on source and
 not index (#48)

---
 .../transcoder/internal/CustomSegments.kt     | 42 +++++++++----------
 .../transcoder/internal/Segment.kt            |  3 +-
 .../thumbnails/DefaultThumbnailsEngine.kt     | 11 +----
 3 files changed, 24 insertions(+), 32 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
index f843902b..91151590 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/CustomSegments.kt
@@ -13,13 +13,12 @@ import com.otaliastudios.transcoder.source.DataSource
 class CustomSegments(
     private val sources: DataSources,
     private val tracks: Tracks,
-    private val factory: (TrackType, Int, TrackStatus, MediaFormat) -> Pipeline,
+    private val factory: (TrackType, DataSource, MediaFormat) -> Pipeline,
 ) {
 
     private val log = Logger("Segments")
     private var currentSegment: Segment? = null
     private var currentSegmentMapKey: String? = null
-    val currentIndex = mutableTrackMapOf(-1, -1)
     private val segmentMap = mutableMapOf<String, Segment?>()
 
     fun hasNext(type: TrackType): Boolean {
@@ -29,8 +28,7 @@ class CustomSegments(
                     " canAdvance=${currentSegment?.canAdvance()}"
         )
         val segment = currentSegment ?: return true // not started
-        val lastIndex = sources.getOrNull(type)?.lastIndex ?: return false // no track!
-        return segment.canAdvance() || segment.index < lastIndex
+        return segment.canAdvance()
     }
 
     fun hasNext() = hasNext(TrackType.VIDEO)
@@ -56,9 +54,9 @@ class CustomSegments(
         val segment = segmentMap[id]
         segment?.let {
             it.release()
-            val source = sources[it.type][it.index]
+            val source = sources[it.type].firstOrNull { it.mediaId() == id }
             if (tracks.active.has(it.type)) {
-                source.releaseTrack(it.type)
+                source?.releaseTrack(it.type)
             }
             segmentMap.remove(id)
             if(currentSegment == segment) {
@@ -70,42 +68,42 @@ class CustomSegments(
     fun release() = destroySegment(true)
 
     private fun tryCreateSegment(id: String): Segment? {
-        val index = sources[TrackType.VIDEO].indexOfFirst { it.mediaId() == id }
         // Return null if out of bounds, either because segments are over or because the
         // source set does not have sources for this track type.
-        val source = sources[TrackType.VIDEO].getOrNull(index) ?: return null
+        val source = sources[TrackType.VIDEO].firstOrNull { it.mediaId() == id } ?: return null
         source.init()
-        log.i("tryCreateSegment(${TrackType.VIDEO}, $index): created!")
+        log.i("tryCreateSegment(${TrackType.VIDEO}, $id): created!")
         if (tracks.active.has(TrackType.VIDEO)) {
             source.selectTrack(TrackType.VIDEO)
         }
         // Update current index before pipeline creation, for other components
         // who check it during pipeline init.
-        currentIndex[TrackType.VIDEO] = index
         val pipeline = factory(
             TrackType.VIDEO,
-            index,
-            tracks.all[TrackType.VIDEO],
+            source,
             tracks.outputFormats[TrackType.VIDEO]
         )
-        return Segment(TrackType.VIDEO, index, pipeline)
+        return Segment(TrackType.VIDEO, -1, pipeline, id)
     }
 
     private fun destroySegment(releaseAll: Boolean = false) {
-        currentSegment?.let {
-            it.release()
-            val source = sources[it.type][it.index]
-            if (tracks.active.has(it.type)) {
-                source.releaseTrack(it.type)
+        currentSegment?.let { segment ->
+            segment.release()
+            val source = sources[segment.type].firstOrNull { it.mediaId() == segment.source }
+            if (tracks.active.has(segment.type)) {
+                source?.releaseTrack(segment.type)
             }
             currentSegmentMapKey?.let {
                 segmentMap.remove(it)
             }
-            if(releaseAll) {
-                currentSegment = null
-                currentSegmentMapKey = null
-                segmentMap.clear()
+            currentSegment = null
+            currentSegmentMapKey = null
+        }
+        if (releaseAll) {
+            segmentMap.forEach {
+                it.value?.release()
             }
+            segmentMap.clear()
         }
     }
     private fun DataSource.init() = if (!isInitialized) initialize() else Unit
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt
index 430b9d52..1a6d8e47 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt
@@ -9,9 +9,10 @@ class Segment(
     val type: TrackType,
     val index: Int,
     private val pipeline: Pipeline,
+    val source: String = "",
 ) {
 
-    private val log = Logger("Segment($type,$index)")
+    private val log = Logger("Segment($type,$index,$source)")
     private var state: State<Unit>? = null
 
     fun advance(): Boolean {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 0815057f..6c171dfd 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -3,11 +3,9 @@
 package com.otaliastudios.transcoder.internal.thumbnails
 
 import android.media.MediaFormat
-import com.otaliastudios.transcoder.common.TrackStatus
 import com.otaliastudios.transcoder.common.TrackType
 import com.otaliastudios.transcoder.internal.CustomSegments
 import com.otaliastudios.transcoder.internal.DataSources
-import com.otaliastudios.transcoder.internal.Timer
 import com.otaliastudios.transcoder.internal.Tracks
 import com.otaliastudios.transcoder.internal.codec.Decoder
 import com.otaliastudios.transcoder.internal.data.Reader
@@ -25,7 +23,6 @@ import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy
 import com.otaliastudios.transcoder.thumbnail.SingleThumbnailRequest
 import com.otaliastudios.transcoder.thumbnail.Thumbnail
 import com.otaliastudios.transcoder.thumbnail.ThumbnailRequest
-import com.otaliastudios.transcoder.time.DefaultTimeInterpolator
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.currentCoroutineContext
@@ -64,8 +61,6 @@ class DefaultThumbnailsEngine(
 
     private val segments = CustomSegments(dataSources, tracks, ::createPipeline)
 
-    private val timer = Timer(DefaultTimeInterpolator(), dataSources, tracks, segments.currentIndex)
-
     init {
         log.i("Created Tracks, Segments, Timer...")
     }
@@ -106,13 +101,11 @@ class DefaultThumbnailsEngine(
 
     private fun createPipeline(
         type: TrackType,
-        index: Int,
-        status: TrackStatus,
+        source: DataSource,
         outputFormat: MediaFormat
     ): Pipeline {
-        val source = dataSources[type][index].ignoringEOS()
         if(VERBOSE) {
-            log.i("Creating pipeline #$index. absoluteUs=${stubs.joinToString { it.toString() }}")
+            log.i("Creating pipeline for $source. absoluteUs=${stubs.joinToString { it.toString() }}")
         }
         shouldSeek = true
         shouldFlush = false

From cfac1551c446314e7eaf91290ba4d0e7708ea641 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Mon, 29 Aug 2022 16:39:07 +0530
Subject: [PATCH 087/101] add source only if exists (#31)

---
 .../thumbnails/DefaultThumbnailsEngine.kt         |  2 +-
 .../transcoder/source/TrimDataSource.java         |  4 ++--
 .../transcoder/source/UriDataSource.java          | 15 +++++++++++++++
 3 files changed, 18 insertions(+), 3 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 6c171dfd..e0e067ec 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -224,7 +224,7 @@ class DefaultThumbnailsEngine(
         }
         dataSources.addVideoDataSource(dataSource)
         tracks.updateTracksInfo()
-        if (tracks.active.has(TrackType.VIDEO)) {
+        if (tracks.active.has(TrackType.VIDEO) && dataSource.getTrackFormat(TrackType.VIDEO) != null) {
             dataSource.selectTrack(TrackType.VIDEO)
         }
     }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/TrimDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/TrimDataSource.java
index 8c17c428..4ad12939 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/TrimDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/TrimDataSource.java
@@ -55,8 +55,8 @@ public void initialize() {
                     "start=" + trimStartUs + ", " +
                     "end=" + trimEndUs + ", " +
                     "duration=" + duration);
-            throw new IllegalArgumentException(
-                    "Trim values cannot be greater than media duration.");
+//            throw new IllegalArgumentException(
+//                    "Trim values cannot be greater than media duration.");
         }
         LOG.i("initialize(): duration=" + duration
                 + " trimStart=" + trimStartUs
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java
index 65c8fc7d..c5dbcfde 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java
@@ -5,6 +5,7 @@
 import android.media.MediaMetadataRetriever;
 import android.net.Uri;
 
+import java.io.FileNotFoundException;
 import java.io.IOException;
 
 import androidx.annotation.NonNull;
@@ -25,11 +26,15 @@ public UriDataSource(@NonNull Context context, @NonNull Uri uri) {
 
     @Override
     protected void initializeExtractor(@NonNull MediaExtractor extractor) throws IOException {
+        if(!checkIfSourceExists(uri))
+            return;
         extractor.setDataSource(context, uri, null);
     }
 
     @Override
     protected void initializeRetriever(@NonNull MediaMetadataRetriever retriever) {
+        if(!checkIfSourceExists(uri))
+            return;
         retriever.setDataSource(context, uri);
     }
 
@@ -37,4 +42,14 @@ protected void initializeRetriever(@NonNull MediaMetadataRetriever retriever) {
     public String mediaId() {
         return uri.toString();
     }
+    private boolean checkIfSourceExists(Uri uri) {
+        try {
+            context.getContentResolver().openInputStream(uri).close();
+            return true;
+        } catch (FileNotFoundException exception) {
+            return false;
+        } catch (IOException e) {
+            return false;
+        }
+    }
 }

From fb46712bfc131357be99227f19a188bc19f59f86 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Mon, 29 Aug 2022 18:36:26 +0530
Subject: [PATCH 088/101] don't persist any request if data source is removed.
 it won't be processed and stuck in loop (#47)

---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt    | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index e0e067ec..76caa6e1 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -312,17 +312,18 @@ class DefaultThumbnailsEngine(
 
     override fun removePosition(sourcePath: String, sourceId: String, positionUs: Long) {
         if (positionUs < 0) {
-            val activeStub = stubs.firstOrNull()?.takeIf { it.request.sourceId() == sourceId }
+//            val activeStub = stubs.firstOrNull()?.takeIf { it.request.sourceId() == sourceId }
             stubs.removeAll {
                 it.request.sourceId() == sourceId
             }
-            if (activeStub != null) {
-                stubs.addFirst(activeStub)
-            }
+//            if (activeStub != null) {
+//                stubs.addFirst(activeStub)
+//            }
             shouldSeek = true
             return
         }
-        val isStubActive = stubs.firstOrNull()?.request?.sourceId() == sourceId && positionUs == stubs.firstOrNull()?.positionUs
+        val isStubActive =
+            stubs.firstOrNull()?.request?.sourceId() == sourceId && positionUs == stubs.firstOrNull()?.positionUs && positionUs > 0
         if (isStubActive) {
             return
         }

From 6271f34f3ab182049325cb878edc941c10ae8e2d Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Mon, 29 Aug 2022 23:59:54 +0530
Subject: [PATCH 089/101] use try catch inside extractor and retriever set
 source to avoid any potential crashes (#50)

---
 .../transcoder/source/UriDataSource.java      | 22 +++++--------------
 1 file changed, 6 insertions(+), 16 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java
index c5dbcfde..16b67a44 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java
@@ -5,7 +5,6 @@
 import android.media.MediaMetadataRetriever;
 import android.net.Uri;
 
-import java.io.FileNotFoundException;
 import java.io.IOException;
 
 import androidx.annotation.NonNull;
@@ -26,30 +25,21 @@ public UriDataSource(@NonNull Context context, @NonNull Uri uri) {
 
     @Override
     protected void initializeExtractor(@NonNull MediaExtractor extractor) throws IOException {
-        if(!checkIfSourceExists(uri))
-            return;
         extractor.setDataSource(context, uri, null);
     }
 
     @Override
     protected void initializeRetriever(@NonNull MediaMetadataRetriever retriever) {
-        if(!checkIfSourceExists(uri))
-            return;
-        retriever.setDataSource(context, uri);
+        try {
+            retriever.setDataSource(context, uri);
+        } catch (IllegalArgumentException ignored) {
+            
+        }
     }
 
     @Override
     public String mediaId() {
         return uri.toString();
     }
-    private boolean checkIfSourceExists(Uri uri) {
-        try {
-            context.getContentResolver().openInputStream(uri).close();
-            return true;
-        } catch (FileNotFoundException exception) {
-            return false;
-        } catch (IOException e) {
-            return false;
-        }
-    }
+
 }

From 4585c6189ee5d58d3c56d998da841fabedf69f30 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Tue, 30 Aug 2022 14:02:46 +0530
Subject: [PATCH 090/101] don't throw exception for extractor setdatasource
 (#51)

---
 .../com/otaliastudios/transcoder/source/DefaultDataSource.java  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index 20c5f70b..58ecb983 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -56,7 +56,7 @@ public void initialize() {
             initializeExtractor(mExtractor);
         } catch (IOException e) {
             LOG.e("Got IOException while trying to open MediaExtractor.", e);
-            throw new RuntimeException(e);
+//            throw new RuntimeException(e);
         }
         mMetadata = new MediaMetadataRetriever();
         initializeRetriever(mMetadata);

From 3ef0d1f13ec7e5a39b530185353311c75db7724b Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Tue, 30 Aug 2022 14:02:54 +0530
Subject: [PATCH 091/101] when converting to id based system, ignoring eos
 source was lost causing old bugs. fixing that (#52)

---
 .../transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt    | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 76caa6e1..07e3267c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -104,6 +104,7 @@ class DefaultThumbnailsEngine(
         source: DataSource,
         outputFormat: MediaFormat
     ): Pipeline {
+        val source = source.ignoringEOS()
         if(VERBOSE) {
             log.i("Creating pipeline for $source. absoluteUs=${stubs.joinToString { it.toString() }}")
         }

From e4536678f4e35be6dc2f3a5a55f4e3ed9b97403f Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Wed, 7 Sep 2022 15:03:16 +0530
Subject: [PATCH 092/101] fallback filmstrip frame request to MMR if for any
 reason decoder fails (#54)

* fallback filmstrip frame request to MMR if for any reason decoder fails

* scale bitmap from MMR

* cleanup bitmap scaling
---
 .../thumbnails/DefaultThumbnailsEngine.kt     | 39 +++++++++++++------
 .../transcoder/source/DataSource.java         | 11 ++++++
 .../transcoder/source/DataSourceWrapper.java  |  6 +++
 .../transcoder/source/DefaultDataSource.java  | 11 ++++++
 4 files changed, 56 insertions(+), 11 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 07e3267c..5f9b1b78 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -274,19 +274,36 @@ class DefaultThumbnailsEngine(
         }
 
         while (currentCoroutineContext().isActive && stubs.isNotEmpty()) {
-            val segment =
-                stubs.firstOrNull()?.request?.sourcePath()?.let { segments.getSegment(it) }
-            if (VERBOSE) {
-                log.i("loop advancing for $segment")
-            }
-            val advanced = try {
-                segment?.advance() ?: false
-            } catch (e: Exception) {
-                if (e !is CancellationException) {
-                    currentCoroutineContext().ensureActive()
+            var advanced = false
+            val stub = stubs.firstOrNull()
+            try {
+                val segment = stub?.request?.sourcePath()?.let { segments.getSegment(it) }
+
+                if (VERBOSE) {
+                    log.i("loop advancing for $segment")
+                }
+                advanced = try {
+                    segment?.advance() ?: false
+                } catch (e: Exception) {
+                    if (e !is CancellationException) {
+                        currentCoroutineContext().ensureActive()
+                    }
+                    throw e
+                }
+            } catch (e: IllegalStateException) {
+                val path = stub?.request?.sourcePath()
+                if (path != null) {
+                    val dataSource = getDataSourceByPath(path)
+                    val bitmap = dataSource?.getFrameAtPosition(stub.positionUs, 150, 150)
+                    if (bitmap != null) {
+                        val thumbnail = Thumbnail(stub.request, stub.positionUs, bitmap)
+                        progress.trySend(thumbnail)
+                        stubs.removeFirst()
+                        advanced = true
+                    }
                 }
-                throw e
             }
+
             // avoid calling hasNext if we advanced.
             val completed = !advanced && !segments.hasNext()
             if (completed || stubs.isEmpty()) {
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
index 030756fa..2be68243 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java
@@ -1,5 +1,6 @@
 package com.otaliastudios.transcoder.source;
 
+import android.graphics.Bitmap;
 import android.media.MediaFormat;
 
 import androidx.annotation.NonNull;
@@ -112,6 +113,15 @@ public interface DataSource {
      */
     void releaseTrack(@NonNull TrackType type);
 
+
+    /**
+     * Returns closest key frame to the required position.
+     * @param positionUs position
+     */
+    default Bitmap getFrameAtPosition(long positionUs, int width, int height) {
+        return null;
+    };
+
     default long requestKeyFrameTimestamps() { return -1;}
 
     default ArrayList<Long> getKeyFrameTimestamps() {
@@ -121,6 +131,7 @@ default ArrayList<Long> getKeyFrameTimestamps() {
     default long getSeekThreshold() {
         return 0;
     }
+
     default String mediaId() { return "";}
     /**
      * Rewinds this source, moving it to its default state.
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSourceWrapper.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSourceWrapper.java
index 7fb6c1c3..a438d8c6 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSourceWrapper.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSourceWrapper.java
@@ -1,6 +1,7 @@
 package com.otaliastudios.transcoder.source;
 
 
+import android.graphics.Bitmap;
 import android.media.MediaFormat;
 
 import androidx.annotation.NonNull;
@@ -97,6 +98,11 @@ public void initialize() {
         }
     }
 
+    @Override
+    public Bitmap getFrameAtPosition(long positionUs, int width, int height) {
+        return mSource.getFrameAtPosition(positionUs, width, height);
+    }
+
     @Override
     public void deinitialize() {
         mSource.deinitialize();
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index 58ecb983..e7ffb308 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -1,8 +1,10 @@
 package com.otaliastudios.transcoder.source;
 
+import android.graphics.Bitmap;
 import android.media.MediaExtractor;
 import android.media.MediaFormat;
 import android.media.MediaMetadataRetriever;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -192,6 +194,15 @@ public void releaseTrack(@NonNull TrackType type) {
         }
     }
 
+    @Override
+    public Bitmap getFrameAtPosition(long positionUs, int width, int height) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+            return mMetadata.getScaledFrameAtTime(positionUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, width, height);
+        }
+        Bitmap bitmap = mMetadata.getFrameAtTime(positionUs);
+        return Bitmap.createScaledBitmap(bitmap, width, height, true);
+
+    }
     protected abstract void initializeExtractor(@NonNull MediaExtractor extractor) throws IOException;
 
     protected abstract void initializeRetriever(@NonNull MediaMetadataRetriever retriever);

From bbe02bc6424bfb02c62518e0d6c75588f21624b8 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Sat, 10 Sep 2022 17:15:00 +0530
Subject: [PATCH 093/101] catch exception when decoder released (#55)

---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt          | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 5f9b1b78..17d54a96 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -309,7 +309,11 @@ class DefaultThumbnailsEngine(
             if (completed || stubs.isEmpty()) {
                 log.i("loop broken $stubs $hasMoreRequestsIncoming")
                 if (!hasMoreRequestsIncoming) {
-                    segments.release()
+                    try {
+                        segments.release()
+                    }
+                    catch (_: IllegalStateException) {
+                    }
                 }
                 break
             } else if (!advanced) {

From 7e7af675ef4e1e28b7da47c94442288e5926aee2 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Thu, 15 Sep 2022 14:07:36 +0530
Subject: [PATCH 094/101] Feat/transcoder event listener (#57)

* replace illegalstate exception with global exception because there's an illegalargument exception too that's happening

* transcoder event listener
---
 .../otaliastudios/transcoder/ThumbnailerOptions.kt  | 11 +++++++++--
 .../transcoder/internal/codec/Decoder.kt            | 13 +++++++++++--
 .../internal/codec/TranscoderEventsListener.kt      | 13 +++++++++++++
 .../internal/thumbnails/DefaultThumbnailsEngine.kt  | 12 +++++++-----
 .../internal/thumbnails/ThumbnailsEngine.kt         |  3 ++-
 5 files changed, 42 insertions(+), 10 deletions(-)
 create mode 100644 lib/src/main/java/com/otaliastudios/transcoder/internal/codec/TranscoderEventsListener.kt

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
index b92b42c8..66418e14 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerOptions.kt
@@ -6,6 +6,7 @@ import android.content.Context
 import android.net.Uri
 import android.os.Handler
 import android.os.Looper
+import com.otaliastudios.transcoder.internal.codec.TranscoderEventsListener
 import com.otaliastudios.transcoder.internal.thumbnails.ThumbnailsEngine
 import com.otaliastudios.transcoder.resize.ExactResizer
 import com.otaliastudios.transcoder.resize.MultiResizer
@@ -24,7 +25,8 @@ class ThumbnailerOptions(
     val rotation: Int,
     val thumbnailRequests: List<ThumbnailRequest>,
     val listener: ThumbnailerListener?,
-    val listenerHandler: Handler
+    val listenerHandler: Handler,
+    val eventListener: TranscoderEventsListener?,
 ) {
 
     class Builder {
@@ -36,6 +38,7 @@ class ThumbnailerOptions(
         private var rotation = 0
         private var listener: ThumbnailerListener? = null
         private var listenerHandler: Handler? = null
+        private var eventListener: TranscoderEventsListener? = null
 
         fun addDataSource(dataSource: DataSource) = this.also {
             dataSources.add(dataSource)
@@ -82,6 +85,9 @@ class ThumbnailerOptions(
         fun setListener(listener: ThumbnailerListener) = this.also {
             this.listener = listener
         }
+        fun setEventListener(listener: TranscoderEventsListener) = this.also {
+            this.eventListener = listener
+        }
 
         fun build(): ThumbnailerOptions {
 //            require(dataSources.isNotEmpty()) {
@@ -97,7 +103,8 @@ class ThumbnailerOptions(
                 rotation = rotation,
                 thumbnailRequests = thumbnailRequests.toList(),
                 listener = listener,
-                listenerHandler = listenerHandler
+                listenerHandler = listenerHandler,
+                eventListener = eventListener
             )
         }
 
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index b49cab36..6718d47f 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -36,6 +36,7 @@ class Decoder(
     private val format: MediaFormat, // source.getTrackFormat(track)
     continuous: Boolean, // relevant if the source sends no-render chunks. should we compensate or not?
     private val useSwFor4K: Boolean = false,
+    private val eventListener: TranscoderEventsListener? = null,
     val shouldFlush: (() -> Boolean)? = null
 ) : QueuedStep<ReaderData, ReaderChannel, DecoderData, DecoderChannel>(), ReaderChannel {
 
@@ -146,6 +147,7 @@ class Decoder(
             codec.configure(format, surface, null, 0)
         }
         catch (e: Exception) {
+            eventListener?.onDecoderConfigureFailure(codec.name, format, e)
             if(BuildConfig.DEBUG) {
                 log.e("Failed while configuring codec ${codec.name} for format $format")
                 logCodecException(e)
@@ -162,6 +164,7 @@ class Decoder(
             codec.start()
         }
         catch (e: Exception) {
+            eventListener?.onDecoderStartFailure(codec.name, format, e)
             if(BuildConfig.DEBUG) {
                 log.e("Failed while starting codec ${codec.name} for format $format")
                 logCodecException(e)
@@ -283,7 +286,13 @@ class Decoder(
 
     override fun release() {
         log.i("release(): releasing codec. dequeuedInputs=$dequeuedInputs dequeuedOutputs=$dequeuedOutputs")
-        codec.stop()
-        codec.release()
+        try {
+            codec.stop()
+            codec.release()
+        }
+        catch (e : Exception) {
+            eventListener?.onDecoderReleaseFailure(codec.name, format, e)
+            throw e
+        }
     }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/TranscoderEventsListener.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/TranscoderEventsListener.kt
new file mode 100644
index 00000000..bfa07afc
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/TranscoderEventsListener.kt
@@ -0,0 +1,13 @@
+package com.otaliastudios.transcoder.internal.codec
+
+import android.media.MediaFormat
+
+interface TranscoderEventsListener {
+
+    fun onDecoderConfigureFailure(codecName: String, format: MediaFormat, exception: Exception)
+
+    fun onDecoderStartFailure(codecName: String, format: MediaFormat, exception: Exception)
+
+    fun onDecoderReleaseFailure(codecName: String, format: MediaFormat, exception: Exception)
+
+}
\ No newline at end of file
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 17d54a96..9779e723 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -8,6 +8,7 @@ import com.otaliastudios.transcoder.internal.CustomSegments
 import com.otaliastudios.transcoder.internal.DataSources
 import com.otaliastudios.transcoder.internal.Tracks
 import com.otaliastudios.transcoder.internal.codec.Decoder
+import com.otaliastudios.transcoder.internal.codec.TranscoderEventsListener
 import com.otaliastudios.transcoder.internal.data.Reader
 import com.otaliastudios.transcoder.internal.data.Seeker
 import com.otaliastudios.transcoder.internal.pipeline.Pipeline
@@ -36,7 +37,8 @@ import java.util.ArrayList
 class DefaultThumbnailsEngine(
     private val dataSources: DataSources,
     private val rotation: Int,
-    resizer: Resizer
+    resizer: Resizer,
+    private val eventListener: TranscoderEventsListener?
 ) : ThumbnailsEngine() {
 
     private var shouldSeek = true
@@ -146,7 +148,7 @@ class DefaultThumbnailsEngine(
                 Pair(seekUs, seek)
             } +
                 Reader(source, type) +
-                Decoder(source.getTrackFormat(type)!!, continuous = false, useSwFor4K = true) {
+                Decoder(source.getTrackFormat(type)!!, continuous = false, useSwFor4K = true, eventListener) {
                     shouldFlush.also {
                         shouldFlush = false
                     }
@@ -290,7 +292,7 @@ class DefaultThumbnailsEngine(
                     }
                     throw e
                 }
-            } catch (e: IllegalStateException) {
+            } catch (e: Exception) {
                 val path = stub?.request?.sourcePath()
                 if (path != null) {
                     val dataSource = getDataSourceByPath(path)
@@ -311,8 +313,8 @@ class DefaultThumbnailsEngine(
                 if (!hasMoreRequestsIncoming) {
                     try {
                         segments.release()
-                    }
-                    catch (_: IllegalStateException) {
+                    } catch (e: IllegalStateException) {
+
                     }
                 }
                 break
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
index e1cc30ad..0fd0c140 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt
@@ -52,7 +52,8 @@ abstract class ThumbnailsEngine {
             val engine = DefaultThumbnailsEngine(
                 dataSources = DataSources(options),
                 rotation = options.rotation,
-                resizer = options.resizer
+                resizer = options.resizer,
+                eventListener = options.eventListener
             )
             return engine
         }

From 2f7b8a716e24643533506e0c11e13fdf4f4e002e Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Fri, 16 Sep 2022 11:01:30 +0530
Subject: [PATCH 095/101] replace illegalstate exception with global exception
 because there's an illegalargument exception too that's happening (#56)


From e81e695aa4623ee367d059c87af6bbdc2b3df429 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Fri, 16 Sep 2022 19:54:59 +0530
Subject: [PATCH 096/101]  if media has only 1 keyframe at 0, infinite loop
 could be created. (#58)

* if media has only 1 keyframe at 0, infinite loop could be created.

* optimise unique list logic

* condition fix

* fix sample time = -1 condition
---
 .../internal/thumbnails/DefaultThumbnailsEngine.kt        | 5 +----
 .../transcoder/source/DefaultDataSource.java              | 8 +++++++-
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
index 9779e723..2b83dc43 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt
@@ -196,7 +196,7 @@ class DefaultThumbnailsEngine(
 
         val nextKeyFrameIndex = when {
             searchIndex >= 0 -> searchIndex
-            searchIndex < 0 -> {
+            else -> {
                 val index = -searchIndex - 1
                 when {
                     index >= keyFrameTimestamps.size -> {
@@ -213,9 +213,6 @@ class DefaultThumbnailsEngine(
                     }
                 }
             }
-            else -> {
-                -1 // will never reach here. kotlin is stupid
-            }
         }
 
         return nextKeyFrameIndex
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
index e7ffb308..c9eefe78 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -130,7 +130,13 @@ public long requestKeyFrameTimestamps() {
                 if (!keyFrameTimestamps.isEmpty() && sampleTime <= keyFrameTimestamps.get(keyFrameTimestamps.size() - 1)) {
                     Collections.sort(keyFrameTimestamps);
                 }
-                keyFrameTimestamps.add(sampleTime);
+                // list is ordered, so only last item can be same.
+                if (keyFrameTimestamps.isEmpty() || keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) != sampleTime) {
+                    keyFrameTimestamps.add(sampleTime);
+                } else {
+                    sampleTime = -1;
+                    break;
+                }
             }
             mExtractor.seekTo(sampleTime + SEEK_THRESHOLD, MediaExtractor.SEEK_TO_NEXT_SYNC);
             lastSampleTime = sampleTime;

From 9218a045b9ddffdcace3a496bc3677e17473a5f1 Mon Sep 17 00:00:00 2001
From: Dhruv Gupta <dhruv2295@gmail.com>
Date: Thu, 22 Sep 2022 15:29:09 +0530
Subject: [PATCH 097/101] catch illegalstate exception silently (#59)

---
 .../java/com/otaliastudios/transcoder/internal/codec/Decoder.kt  | 1 -
 1 file changed, 1 deletion(-)

diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
index 6718d47f..cbf22683 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt
@@ -292,7 +292,6 @@ class Decoder(
         }
         catch (e : Exception) {
             eventListener?.onDecoderReleaseFailure(codec.name, format, e)
-            throw e
         }
     }
 }

From 64bb924ed6501d7bb31414cce5f7421bc3ebe2e4 Mon Sep 17 00:00:00 2001
From: Mayank Kharbanda <mayank@invideo.io>
Date: Mon, 31 Oct 2022 19:00:17 +0530
Subject: [PATCH 098/101] Enables buildConfig buildFeature (#60)

---
 lib/build.gradle.kts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index 5fef8d19..4f0513b9 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -19,6 +19,9 @@ android {
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
     }
     buildTypes["release"].isMinifyEnabled = false
+    buildFeatures {
+        buildConfig = true
+    }
 }
 
 dependencies {

From 2ae5ff79d6c0229dde34031ad77240c684cf352a Mon Sep 17 00:00:00 2001
From: Mayank Kharbanda <mayank@invideo.io>
Date: Thu, 29 Dec 2022 20:08:22 +0530
Subject: [PATCH 099/101] Bump kotlin to 1.7.20 and AGP to 7.3.1 (#61)

---
 build.gradle.kts     |  4 ++--
 lib/build.gradle.kts | 10 ++++------
 2 files changed, 6 insertions(+), 8 deletions(-)

diff --git a/build.gradle.kts b/build.gradle.kts
index 794cc83b..26b2dfd1 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -11,8 +11,8 @@ buildscript {
     }
 
     dependencies {
-        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31")
-        classpath("com.android.tools.build:gradle:4.1.3")
+        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
+        classpath("com.android.tools.build:gradle:7.3.1")
         classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.4.30") // publisher uses 1.4.20 which goes OOM
         classpath("io.deepmedia.tools:publisher:0.5.0")
     }
diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index 4f0513b9..64babb43 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -12,10 +12,8 @@ plugins {
 android {
     setCompileSdkVersion(property("compileSdkVersion") as Int)
     defaultConfig {
-        setMinSdkVersion(property("minSdkVersion") as Int)
-        setTargetSdkVersion(property("targetSdkVersion") as Int)
-        versionCode = 1
-        versionName = "0.10.3"
+        minSdk = (property("minSdkVersion") as Int)
+        targetSdk = (property("targetSdkVersion") as Int)
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
     }
     buildTypes["release"].isMinifyEnabled = false
@@ -26,8 +24,8 @@ android {
 
 dependencies {
     api("com.otaliastudios.opengl:egloo:0.6.0")
-    api("androidx.annotation:annotation:1.1.0")
-    api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
+    api("androidx.annotation:annotation:1.3.0")
+    api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
 
     androidTestImplementation("androidx.test:runner:1.3.0")
     androidTestImplementation("androidx.test:rules:1.3.0")

From 7609a0f244395613c02a803e1549de6b3309d725 Mon Sep 17 00:00:00 2001
From: Mayank Kharbanda <mayank@invideo.io>
Date: Tue, 24 Jan 2023 15:54:40 +0530
Subject: [PATCH 100/101] Update Kotlin to 1.8.0 and AGP to 7.4.0 (#62)

---
 build.gradle.kts     | 11 +++++------
 lib/build.gradle.kts |  8 ++++----
 2 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/build.gradle.kts b/build.gradle.kts
index 26b2dfd1..303461c6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -7,14 +7,14 @@ buildscript {
     repositories {
         google()
         mavenCentral()
-        jcenter()
+        gradlePluginPortal()
     }
 
     dependencies {
-        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
-        classpath("com.android.tools.build:gradle:7.3.1")
-        classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.4.30") // publisher uses 1.4.20 which goes OOM
-        classpath("io.deepmedia.tools:publisher:0.5.0")
+        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
+        classpath("com.android.tools.build:gradle:7.4.0")
+//        classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.4.30") // publisher uses 1.4.20 which goes OOM
+//        classpath("io.deepmedia.tools:publisher:0.5.0")
     }
 }
 
@@ -22,7 +22,6 @@ allprojects {
     repositories {
         google()
         mavenCentral()
-        jcenter()
     }
 }
 
diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index 64babb43..c30414fd 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -1,7 +1,7 @@
-import io.deepmedia.tools.publisher.common.GithubScm
-import io.deepmedia.tools.publisher.common.License
-import io.deepmedia.tools.publisher.common.Release
-import io.deepmedia.tools.publisher.sonatype.Sonatype
+//import io.deepmedia.tools.publisher.common.GithubScm
+//import io.deepmedia.tools.publisher.common.License
+//import io.deepmedia.tools.publisher.common.Release
+//import io.deepmedia.tools.publisher.sonatype.Sonatype
 
 plugins {
     id("com.android.library")

From d9cb529a41564f19170a1fe347afc4a926915cd0 Mon Sep 17 00:00:00 2001
From: ManishPatel2611 <87066875+ManishPatel2611@users.noreply.github.com>
Date: Fri, 5 Apr 2024 15:21:44 +0530
Subject: [PATCH 101/101] Add namespace and remove packageId from manifest
 (#63)

---
 lib/build.gradle.kts             | 1 +
 lib/src/main/AndroidManifest.xml | 3 +--
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index c30414fd..1622ffe2 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -15,6 +15,7 @@ android {
         minSdk = (property("minSdkVersion") as Int)
         targetSdk = (property("targetSdkVersion") as Int)
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+        namespace = "com.otaliastudios.transcoder"
     }
     buildTypes["release"].isMinifyEnabled = false
     buildFeatures {
diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml
index f62d05a4..925315a4 100644
--- a/lib/src/main/AndroidManifest.xml
+++ b/lib/src/main/AndroidManifest.xml
@@ -1,5 +1,4 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.otaliastudios.transcoder">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
     <application />
 
