diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
index 0df8b1dc..7cfaf5e8 100644
--- a/.github/CODE_OF_CONDUCT.md
+++ b/.github/CODE_OF_CONDUCT.md
@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
 
 ## Enforcement
 
-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mat.iavarone@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@deepmedia.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
 
 Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
 
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 89da4fab..b9763c60 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1 +1,30 @@
-Contributing guidelines are [hosted here](https://natario1.github.io/Transcoder/extra/contributing).
\ No newline at end of file
+
+Everyone is welcome to contribute with suggestions or pull requests. We are grateful to anyone who will contribute with fixes, features or feature requests. 
+
+### Bug reports
+
+Please make sure to fill the bug report issue template on GitHub, if applicable.
+We highly recommend to try to reproduce the bug in the demo app, as this helps a lot in debugging
+and excludes programming errors from your side.
+
+Make sure to include:
+
+- A clear and concise description of what the bug is
+- Transcoder version, device type, Android API level
+- Exact steps to reproduce the issue
+- Description of the expected behavior
+- The original media file(s) that manifest the problem
+
+Recommended extras:
+
+- LogCat logs (use `Logger.setLogLevel(LEVEL_VERBOSE)` to print all)
+- Link to a GitHub repo where the bug is reproducible
+
+### Pull Requests
+
+Please open an issue first!
+
+Unless your PR is a simple fix (typos, documentation, bugs with obvious solution), opening an issue
+will let us discuss the problem, take design decisions and have a reference to the issue description.
+
+If you can, please write tests. We are planning to work on improving the library test coverage soon.
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 47471b0d..00000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-# These are supported funding model platforms
-
-github: [natario1]
-patreon: # Replace with a single Patreon username
-open_collective: # Replace with a single Open Collective username
-ko_fi: # Replace with a single Ko-fi username
-tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
-community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
-liberapay: # Replace with a single Liberapay username
-issuehunt: # Replace with a single IssueHunt username
-otechie: # Replace with a single Otechie username
-custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d1a19aad..5744e083 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,5 +1,4 @@
 # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
-# Renaming ? Change the README badge.
 name: Build
 on:
   push:
@@ -11,44 +10,55 @@ jobs:
     name: Base Checks
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v1
-      - uses: actions/setup-java@v1
+      - uses: actions/checkout@v4
+      - uses: actions/setup-java@v4
         with:
-          java-version: 1.8
+          java-version: 17
+          distribution: temurin
+          cache: gradle
       - name: Perform base checks
-        run: ./gradlew demo:assembleDebug lib:publishToDirectory
+        run: ./gradlew demo:assembleDebug lib:deployLocal --stacktrace
   ANDROID_EMULATOR_TESTS:
     name: Emulator Tests
-    runs-on: macOS-latest
+    runs-on: ubuntu-latest
     strategy:
       fail-fast: false
       matrix:
-        EMULATOR_API: [22, 25, 28]
-        include:
-          - EMULATOR_API: 28
-            EMULATOR_ARCH: x86_64
-          - EMULATOR_API: 25
-            EMULATOR_ARCH: x86
-          - EMULATOR_API: 22
-            EMULATOR_ARCH: x86
+        EMULATOR_API: [24, 27, 29, 31, 34]
     steps:
-      - uses: actions/checkout@v1
-      - uses: actions/setup-java@v1
+      - uses: actions/checkout@v4
+      - uses: actions/setup-java@v4
         with:
-          java-version: 1.8
+          java-version: 17
+          distribution: temurin
+          cache: gradle
+
+      - name: Enable KVM group perms
+        run: |
+          echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+          sudo udevadm control --reload-rules
+          sudo udevadm trigger --name-match=kvm
+
       - name: Execute emulator tests
         timeout-minutes: 30
-        uses: reactivecircus/android-emulator-runner@v2.2.0
+        uses: reactivecircus/android-emulator-runner@v2
         with:
           api-level: ${{ matrix.EMULATOR_API }}
-          arch: ${{ matrix.EMULATOR_ARCH }}
-          disable-animations: true
-          profile: Nexus 5X
+          arch: x86_64
+          profile: Nexus 6
           emulator-options: -no-snapshot -no-window -no-boot-anim -camera-back none -camera-front none -gpu swiftshader_indirect
-          emulator-build: 6031357
-          script: ./.github/workflows/emulator_script.sh
+          script: ./.github/workflows/emulator_script.sh logcat_${{ matrix.EMULATOR_API }}.txt
+
+      - name: Upload emulator logs
+        uses: actions/upload-artifact@v4
+        if: always()
+        with:
+          name: emulator_logs_${{ matrix.EMULATOR_API }}
+          path: ./logcat_${{ matrix.EMULATOR_API }}.txt
+
       - name: Upload emulator tests artifact
-        uses: actions/upload-artifact@v1
+        uses: actions/upload-artifact@v4
+        if: always()
         with:
           name: emulator_tests_${{ matrix.EMULATOR_API }}
-          path: ./lib/build/outputs/code_coverage/debugAndroidTest/connected
\ No newline at end of file
+          path: ./lib/build/reports/androidTests/connected/debug/
\ No newline at end of file
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 30443aae..812d3844 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -5,17 +5,23 @@ on:
     types: [published]
 jobs:
   MAVEN_UPLOAD:
-    name: Maven Upload
+    name: Maven Central Upload
     runs-on: ubuntu-latest
     env:
       SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
       SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
       SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
       SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
+      GHUB_USER: ${{ secrets.GHUB_USER }}
+      GHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GHUB_PERSONAL_ACCESS_TOKEN }}
     steps:
-      - uses: actions/checkout@v1
-      - uses: actions/setup-java@v1
+      - uses: actions/checkout@v4
+      - uses: actions/setup-java@v4
         with:
-          java-version: 1.8
-      - name: Perform maven upload
-        run: ./gradlew publishToSonatype
+          java-version: 17
+          distribution: temurin
+          cache: gradle
+      - name: Publish to Maven Central
+        run: ./gradlew deployNexus --stacktrace
+      - name: Publish to GitHub Packages
+        run: ./gradlew deployGithub --stacktrace
diff --git a/.github/workflows/emulator_script.sh b/.github/workflows/emulator_script.sh
index 921467b1..58a57fb1 100755
--- a/.github/workflows/emulator_script.sh
+++ b/.github/workflows/emulator_script.sh
@@ -1,8 +1,6 @@
 #!/usr/bin/env bash
-ADB_TAGS="Transcoder:I Engine:I"
-ADB_TAGS="$ADB_TAGS DefaultVideoStrategy:I DefaultAudioStrategy:I"
-ADB_TAGS="$ADB_TAGS VideoDecoderOutput:I VideoFrameDropper:I"
-ADB_TAGS="$ADB_TAGS AudioEngine:I"
 adb logcat -c
-adb logcat $ADB_TAGS *:E -v color &
-./gradlew lib:connectedCheck
\ No newline at end of file
+adb logcat *:V > "$1" &
+LOGCAT_PID=$!
+trap "kill $LOGCAT_PID" EXIT
+./gradlew lib:connectedCheck --stacktrace
\ No newline at end of file
diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml
index dae1ea77..e1914ec5 100644
--- a/.github/workflows/snapshot.yml
+++ b/.github/workflows/snapshot.yml
@@ -14,10 +14,14 @@ jobs:
       SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
       SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
       SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
+      GHUB_USER: ${{ secrets.GHUB_USER }}
+      GHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GHUB_PERSONAL_ACCESS_TOKEN }}
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-java@v1
+      - uses: actions/checkout@v4
+      - uses: actions/setup-java@v4
         with:
-          java-version: 1.8
-      - name: Publish sonatype snapshot
-        run: ./gradlew publishToSonatypeSnapshot
\ No newline at end of file
+          java-version: 17
+          distribution: temurin
+          cache: gradle
+      - name: Publish nexus snapshot
+        run: ./gradlew deployNexusSnapshot --stacktrace
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 79859d12..d680f1ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
 .gradle
 /local.properties
-/.idea
 .DS_Store
 /build
 *.iml
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..f0c75ba1
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+androidTestResultsUserPreferences.xml
+deploymentTargetDropDown.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 00000000..b589d56e
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <bytecodeTargetLevel target="17" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 00000000..75ba98d7
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="GradleMigrationSettings" migrationVersion="1" />
+  <component name="GradleSettings">
+    <option name="linkedExternalProjectsSettings">
+      <GradleProjectSettings>
+        <option name="externalProjectPath" value="$PROJECT_DIR$" />
+        <option name="gradleJvm" value="jbr-17" />
+        <option name="modules">
+          <set>
+            <option value="$PROJECT_DIR$" />
+            <option value="$PROJECT_DIR$/demo" />
+            <option value="$PROJECT_DIR$/lib" />
+            <option value="$PROJECT_DIR$/lib-legacy" />
+          </set>
+        </option>
+      </GradleProjectSettings>
+    </option>
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 00000000..6d0ee1c2
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="KotlinJpsPluginSettings">
+    <option name="version" value="2.0.0" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..2189e580
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+<project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="11" project-jdk-type="JavaSDK" />
+</project>
\ No newline at end of file
diff --git a/.idea/runConfigurations/deployLocal.xml b/.idea/runConfigurations/deployLocal.xml
new file mode 100644
index 00000000..ca2317c2
--- /dev/null
+++ b/.idea/runConfigurations/deployLocal.xml
@@ -0,0 +1,24 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="deployLocal" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="--stacktrace" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="deployLocal" />
+        </list>
+      </option>
+      <option name="vmOptions" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <RunAsTest>false</RunAsTest>
+    <method v="2" />
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..35eb1ddf
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 8dada3ed..119434ee 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright {yyyy} {name of copyright owner}
+   Copyright 2024 DeepMedia Srl
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 8aa42823..4b553a52 100644
--- a/README.md
+++ b/README.md
@@ -1,93 +1,60 @@
-[![Build Status](https://github.com/natario1/Transcoder/workflows/Build/badge.svg?event=push)](https://github.com/natario1/Transcoder/actions)
-[![Release](https://img.shields.io/github/release/natario1/Transcoder.svg)](https://github.com/natario1/Transcoder/releases)
-[![Issues](https://img.shields.io/github/issues-raw/natario1/Transcoder.svg)](https://github.com/natario1/Transcoder/issues)
+[![Build Status](https://github.com/deepmedia/Transcoder/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/deepmedia/Transcoder/actions)
+[![Release](https://img.shields.io/github/release/deepmedia/Transcoder.svg)](https://github.com/deepmedia/Transcoder/releases)
+[![Issues](https://img.shields.io/github/issues-raw/deepmedia/Transcoder.svg)](https://github.com/deepmedia/Transcoder/issues)
 
-&#10240;  <!-- Hack to add whitespace -->
-
-<p align="center">
-  <img src="docs/static/banner.png" width="100%">
-</p>
-
-*Looking for a powerful camera library to take videos? Take a look at our [CameraView](https://github.com/natario1/CameraView).*
-
-*Need support, consulting, or have any other business-related question? Feel free to <a href="mailto:mat.iavarone@gmail.com">get in touch</a>.*
-
-*Like the project, make profit from it, or simply want to thank back? Please consider [sponsoring me](https://github.com/sponsors/natario1)!*
+![Project logo](assets/logo-256.png)
 
 # 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+.
-
-```groovy
-implementation 'com.otaliastudios:transcoder:0.10.3'
-```
+Android codecs available on the device. Works on API 21+.
 
 - Fast transcoding to AAC/AVC
 - Hardware accelerated
 - Convenient, fluent API
 - Thumbnails support
-- Concatenate multiple video and audio tracks [[docs]](https://natario1.github.io/Transcoder/docs/concatenation)
-- Clip or trim video segments [[docs]](https://natario1.github.io/Transcoder/docs/clipping)
-- Choose output size, with automatic cropping [[docs]](https://natario1.github.io/Transcoder/docs/track-strategies#video-size)
-- Choose output rotation [[docs]](https://natario1.github.io/Transcoder/docs/advanced-options#video-rotation) 
-- Choose output speed [[docs]](https://natario1.github.io/Transcoder/docs/advanced-options#video-speed)
-- Choose output frame rate [[docs]](https://natario1.github.io/Transcoder/docs/track-strategies#other-options)
-- Choose output audio channels [[docs]](https://natario1.github.io/Transcoder/docs/track-strategies#audio-strategies)
-- Choose output audio sample rate [[docs]](https://natario1.github.io/Transcoder/docs/track-strategies#audio-strategies)
-- Override frames timestamp, e.g. to slow down the middle part of the video [[docs]](https://natario1.github.io/Transcoder/docs/advanced-options#time-interpolation) 
-- Error handling [[docs]](https://natario1.github.io/Transcoder/docs/events)
-- Configurable validators to e.g. avoid transcoding if the source is already compressed enough [[docs]](https://natario1.github.io/Transcoder/docs/validators)
-- Configurable video and audio strategies [[docs]](https://natario1.github.io/Transcoder/docs/track-strategies)
-
-&#10240;  <!-- Hack to add whitespace -->
-
-<p align="center">
-  <img src="docs/static/screenshot-1.png" width="250" hspace="25"><img src="docs/static/screenshot-2.png" width="250" hspace="25">
-</p>
-
-&#10240;  <!-- Hack to add whitespace -->
+- Concatenate multiple video and audio tracks [[docs]](https://opensource.deepmedia.io/transcoder/concatenation)
+- Clip or trim video segments [[docs]](https://opensource.deepmedia.io/transcoder/clipping)
+- Choose output size, with automatic cropping [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies#video-size)
+- Choose output rotation [[docs]](https://opensource.deepmedia.io/transcoder/advanced-options#video-rotation)
+- Choose output speed [[docs]](https://opensource.deepmedia.io/transcoder/advanced-options#video-speed)
+- Choose output frame rate [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies#other-options)
+- Choose output audio channels [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies#audio-strategies)
+- Choose output audio sample rate [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies#audio-strategies)
+- Override frames timestamp, e.g. to slow down the middle part of the video [[docs]](https://opensource.deepmedia.io/transcoder/advanced-options#time-interpolation)
+- Error handling [[docs]](https://opensource.deepmedia.io/transcoder/events)
+- Configurable validators to e.g. avoid transcoding if the source is already compressed enough [[docs]](https://opensource.deepmedia.io/transcoder/validators)
+- Configurable video and audio strategies [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies)
+
+```kotlin
+// build.gradle.kts
+dependencies {
+    implementation("io.deepmedia.community:transcoder-android:0.11.2")
+}
+```
 
 *This project started as a fork of [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder).
-With respect to the source project, which misses most of the functionality listed above, 
-we have also fixed a huge number of bugs and are much less conservative when choosing options 
+With respect to the source project, which misses most of the functionality listed above,
+we have also fixed a huge number of bugs and are much less conservative when choosing options
 that might not be supported. The source project will always throw - for example, accepting only 16:9,
 AVC Baseline Profile videos - we prefer to try and let the codec fail if it wants to*.
 
-## Support
-
-If you like the project, make profit from it, or simply want to thank back, please consider 
-[sponsoring me](https://github.com/sponsors/natario1) through the GitHub Sponsors program! 
-You can have your company logo here, get private support hours or simply help me push this forward. 
-
-Transcoder is trusted and supported by [ShareChat](https://sharechat.com/), a social media app with
-over 100 million downloads. 
+*Transcoder is trusted and supported by [ShareChat](https://sharechat.com/), a social media app with
+over 100 million downloads.*
 
-<p align="center">
-  <img src="docs/static/sharechat.png" width="100%">
-</p>
+Please check out [the official website](https://opensource.deepmedia.io/transcoder) for setup instructions and documentation.
+You may also check the demo app (under `/demo`) for a complete example.
 
-Feel free to <a href="mailto:mat.iavarone@gmail.com">contact me</a> for support, consulting or any 
-other business-related question.
-
-## Setup
-
-Please read the [official website](https://natario1.github.io/Transcoder) for setup instructions and documentation.
-You might also be interested in our [changelog](https://natario1.github.io/Transcoder/about/changelog). 
-Using Transcoder is extremely simple:
-
-```java
+```kotlin
 Transcoder.into(filePath)
-        .addDataSource(context, uri) // or...
-        .addDataSource(filePath) // or...
-        .addDataSource(fileDescriptor) // or...
-        .addDataSource(dataSource)
-        .setListener(new TranscoderListener() {
-             public void onTranscodeProgress(double progress) {}
-             public void onTranscodeCompleted(int successCode) {}
-             public void onTranscodeCanceled() {}
-             public void onTranscodeFailed(@NonNull Throwable exception) {}
-        }).transcode()
+    .addDataSource(context, uri) // or...
+    .addDataSource(filePath) // or...
+    .addDataSource(fileDescriptor) // or...
+    .addDataSource(dataSource)
+    .setListener(object : TranscoderListener {
+         override fun onTranscodeProgress(progress: Double) = Unit
+         override fun onTranscodeCompleted(successCode: Int) = Unit
+         override fun onTranscodeCanceled() = Unit
+         override fun onTranscodeFailed(exception: Throwable) = Unit
+    }).transcode()
 ```
-
-Take a look at the demo app for a complete example.
\ No newline at end of file
diff --git a/assets/logo-256.png b/assets/logo-256.png
new file mode 100644
index 00000000..932c421a
Binary files /dev/null and b/assets/logo-256.png differ
diff --git a/build.gradle.kts b/build.gradle.kts
index a4bb9799..5816c9a9 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,31 +1,6 @@
-buildscript {
-
-    extra["minSdkVersion"] = 18
-    extra["compileSdkVersion"] = 30
-    extra["targetSdkVersion"] = 30
-    
-    repositories {
-        google()
-        mavenCentral()
-        jcenter()
-    }
-
-    dependencies {
-        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31")
-        classpath("com.android.tools.build:gradle:4.1.2")
-        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")
-    }
-}
-
-allprojects {
-    repositories {
-        google()
-        mavenCentral()
-        jcenter()
-    }
-}
-
-tasks.register("clean", Delete::class) {
-    delete(buildDir)
+plugins {
+    kotlin("android") version "2.0.0" apply false
+    id("com.android.library") version "8.2.2" apply false
+    id("com.android.application") version "8.2.2" apply false
+    id("io.deepmedia.tools.deployer") version "0.14.0" apply false
 }
\ No newline at end of file
diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts
index e493dd7c..93fcf8ac 100644
--- a/demo/build.gradle.kts
+++ b/demo/build.gradle.kts
@@ -1,27 +1,21 @@
 plugins {
     id("com.android.application")
-    id("kotlin-android")
+    kotlin("android")
 }
 
 android {
-    setCompileSdkVersion(property("compileSdkVersion") as Int)
-
+    namespace = "com.otaliastudios.transcoder.demo"
+    compileSdk = 34
     defaultConfig {
-        applicationId = "com.otaliastudios.transcoder.demo"
-        setMinSdkVersion(property("minSdkVersion") as Int)
-        setTargetSdkVersion(property("targetSdkVersion") as Int)
+        minSdk = 21
+        targetSdk = 34
         versionCode = 1
         versionName = "1.0"
     }
-
-    compileOptions {
-        sourceCompatibility = JavaVersion.VERSION_1_8
-        targetCompatibility = JavaVersion.VERSION_1_8
-    }
 }
 
 dependencies {
     implementation(project(":lib"))
-    implementation("com.google.android.material:material:1.3.0")
-    implementation("androidx.appcompat:appcompat:1.2.0")
+    implementation("com.google.android.material:material:1.12.0")
+    implementation("androidx.appcompat:appcompat:1.7.0")
 }
diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml
index 3b363c11..5da9cd95 100644
--- a/demo/src/main/AndroidManifest.xml
+++ b/demo/src/main/AndroidManifest.xml
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    package="com.otaliastudios.transcoder.demo">
+    xmlns:tools="http://schemas.android.com/tools">
 
     <uses-permission
         android:name="android.permission.WRITE_EXTERNAL_STORAGE"
@@ -16,7 +15,8 @@
 
         <activity
             android:name=".TranscoderActivity"
-            android:label="Transcoder">
+            android:label="Transcoder"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -25,7 +25,8 @@
 
         <activity
             android:name=".ThumbnailerActivity"
-            android:label="Thumbnailer">
+            android:label="Thumbnailer"
+            android:exported="true">
             <intent-filter>
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
diff --git a/demo/src/main/java/com/otaliastudios/transcoder/demo/ThumbnailerActivity.java b/demo/src/main/java/com/otaliastudios/transcoder/demo/ThumbnailerActivity.java
index 982aa31f..3cdb367e 100644
--- a/demo/src/main/java/com/otaliastudios/transcoder/demo/ThumbnailerActivity.java
+++ b/demo/src/main/java/com/otaliastudios/transcoder/demo/ThumbnailerActivity.java
@@ -35,6 +35,8 @@
 import com.otaliastudios.transcoder.thumbnail.Thumbnail;
 import com.otaliastudios.transcoder.thumbnail.UniformThumbnailRequest;
 
+import com.otaliastudios.transcoder.demo.R;
+
 import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
@@ -142,30 +144,27 @@ private void thumbnails(@NonNull Uri... uris) {
             builder.addDataSource(source);
         }
 
-        float aspectRatio;
-        switch (mVideoAspectGroup.getCheckedRadioButtonId()) {
-            case R.id.aspect_169: aspectRatio = 16F / 9F; break;
-            case R.id.aspect_43: aspectRatio = 4F / 3F; break;
-            case R.id.aspect_square: aspectRatio = 1F; break;
-            default: aspectRatio = 0F;
-        }
+        float aspectRatio = 0F;
+        int aspectRatioId = mVideoAspectGroup.getCheckedRadioButtonId();
+        if (aspectRatioId == R.id.aspect_169) aspectRatio = 16F / 9F;
+        else if (aspectRatioId == R.id.aspect_43) aspectRatio = 4F / 3F;
+        else if (aspectRatioId == R.id.aspect_square) aspectRatio = 1F;
         if (aspectRatio > 0) {
             builder.addResizer(new AspectRatioResizer(aspectRatio));
         }
-        float fraction;
-        switch (mVideoResolutionGroup.getCheckedRadioButtonId()) {
-            case R.id.resolution_half: fraction = 0.5F; break;
-            case R.id.resolution_third: fraction = 1F / 3F; break;
-            default: fraction = 1F;
-        }
+
+        float fraction = 1F;
+        int fractionId = mVideoResolutionGroup.getCheckedRadioButtonId();
+        if (fractionId == R.id.resolution_half) fraction = 0.5F;
+        else if (fractionId == R.id.resolution_third) fraction = 1F / 3F;
         builder.addResizer(new FractionResizer(fraction));
-        int rotation;
-        switch (mVideoRotationGroup.getCheckedRadioButtonId()) {
-            case R.id.rotation_90: rotation = 90; break;
-            case R.id.rotation_180: rotation = 180; break;
-            case R.id.rotation_270: rotation = 270; break;
-            default: rotation = 0;
-        }
+
+
+        int rotation = 0;
+        int rotationId = mVideoRotationGroup.getCheckedRadioButtonId();
+        if (rotationId == R.id.rotation_90) rotation = 90;
+        else if (rotationId == R.id.rotation_180) rotation = 180;
+        else if (rotationId == R.id.rotation_270) rotation = 270;
         builder.setRotation(rotation);
 
         // Launch the transcoding operation.
diff --git a/demo/src/main/java/com/otaliastudios/transcoder/demo/TranscoderActivity.java b/demo/src/main/java/com/otaliastudios/transcoder/demo/TranscoderActivity.java
index ea904de9..c07ef4bb 100644
--- a/demo/src/main/java/com/otaliastudios/transcoder/demo/TranscoderActivity.java
+++ b/demo/src/main/java/com/otaliastudios/transcoder/demo/TranscoderActivity.java
@@ -159,24 +159,21 @@ protected void onCreate(Bundle savedInstanceState) {
     }
 
     private void syncParameters() {
-        int channels;
-        switch (mAudioChannelsGroup.getCheckedRadioButtonId()) {
-            case R.id.channels_mono: channels = 1; break;
-            case R.id.channels_stereo: channels = 2; break;
-            default: channels = DefaultAudioStrategy.CHANNELS_AS_INPUT;
-        }
-        int sampleRate;
-        switch (mAudioSampleRateGroup.getCheckedRadioButtonId()) {
-            case R.id.sampleRate_32: sampleRate = 32000; break;
-            case R.id.sampleRate_48: sampleRate = 48000; break;
-            default: sampleRate = DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT;
-        }
-        boolean removeAudio;
-        switch (mAudioReplaceGroup.getCheckedRadioButtonId()) {
-            case R.id.replace_remove: removeAudio = true; break;
-            case R.id.replace_yes: removeAudio = false; break;
-            default: removeAudio = false;
-        }
+        int channels = DefaultAudioStrategy.CHANNELS_AS_INPUT;
+        int channelsId = mAudioChannelsGroup.getCheckedRadioButtonId();
+        if (channelsId == R.id.channels_mono) channels = 1;
+        else if (channelsId == R.id.channels_stereo) channels = 2;
+
+        int sampleRate = DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT;
+        int sampleRateId = mAudioSampleRateGroup.getCheckedRadioButtonId();
+        if (sampleRateId == R.id.sampleRate_32) sampleRate = 32000;
+        else if (sampleRateId == R.id.sampleRate_48) sampleRate = 48000;
+
+        boolean removeAudio = false;
+        int removeAudioId = mAudioReplaceGroup.getCheckedRadioButtonId();
+        if (removeAudioId == R.id.replace_remove) removeAudio = true;
+        else if (removeAudioId == R.id.replace_yes) removeAudio = false;
+
         if (removeAudio) {
             mTranscodeAudioStrategy = new RemoveTrackStrategy();
         } else {
@@ -186,26 +183,23 @@ private void syncParameters() {
                     .build();
         }
 
-        int frames;
-        switch (mVideoFramesGroup.getCheckedRadioButtonId()) {
-            case R.id.frames_24: frames = 24; break;
-            case R.id.frames_30: frames = 30; break;
-            case R.id.frames_60: frames = 60; break;
-            default: frames = DefaultVideoStrategy.DEFAULT_FRAME_RATE;
-        }
-        float fraction;
-        switch (mVideoResolutionGroup.getCheckedRadioButtonId()) {
-            case R.id.resolution_half: fraction = 0.5F; break;
-            case R.id.resolution_third: fraction = 1F / 3F; break;
-            default: fraction = 1F;
-        }
-        float aspectRatio;
-        switch (mVideoAspectGroup.getCheckedRadioButtonId()) {
-            case R.id.aspect_169: aspectRatio = 16F / 9F; break;
-            case R.id.aspect_43: aspectRatio = 4F / 3F; break;
-            case R.id.aspect_square: aspectRatio = 1F; break;
-            default: aspectRatio = 0F;
-        }
+        int frames = DefaultVideoStrategy.DEFAULT_FRAME_RATE;
+        int framesId = mVideoFramesGroup.getCheckedRadioButtonId();
+        if (framesId == R.id.frames_24) frames = 24;
+        else if (framesId == R.id.frames_30) frames = 30;
+        else if (framesId == R.id.frames_60) frames = 60;
+
+        float fraction = 1F;
+        int fractionId = mVideoResolutionGroup.getCheckedRadioButtonId();
+        if (fractionId == R.id.resolution_half) fraction = 0.5F;
+        else if (fractionId == R.id.resolution_third) fraction = 1F / 3F;
+
+        float aspectRatio = 0F;
+        int aspectRatioId = mVideoAspectGroup.getCheckedRadioButtonId();
+        if (aspectRatioId == R.id.aspect_169) aspectRatio = 16F / 9F;
+        else if (aspectRatioId == R.id.aspect_43) aspectRatio = 4F / 3F;
+        else if (aspectRatioId == R.id.aspect_square) aspectRatio = 1F;
+
         mTranscodeVideoStrategy = new DefaultVideoStrategy.Builder()
                 .addResizer(aspectRatio > 0 ? new AspectRatioResizer(aspectRatio) : new PassThroughResizer())
                 .addResizer(new FractionResizer(fraction))
@@ -275,20 +269,16 @@ private void transcode(@NonNull Uri... uris) {
             return;
         }
 
-        int rotation;
-        switch (mVideoRotationGroup.getCheckedRadioButtonId()) {
-            case R.id.rotation_90: rotation = 90; break;
-            case R.id.rotation_180: rotation = 180; break;
-            case R.id.rotation_270: rotation = 270; break;
-            default: rotation = 0;
-        }
+        int rotation = 0;
+        int rotationId = mVideoRotationGroup.getCheckedRadioButtonId();
+        if (rotationId == R.id.rotation_90) rotation = 90;
+        else if (rotationId == R.id.rotation_180) rotation = 180;
+        else if (rotationId == R.id.rotation_270) rotation = 270;
 
-        float speed;
-        switch (mSpeedGroup.getCheckedRadioButtonId()) {
-            case R.id.speed_05x: speed = 0.5F; break;
-            case R.id.speed_2x: speed = 2F; break;
-            default: speed = 1F;
-        }
+        float speed = 1F;
+        int speedId = mSpeedGroup.getCheckedRadioButtonId();
+        if (speedId == R.id.speed_05x) speed = 0.5F;
+        else if (speedId == R.id.speed_2x) speed = 2F;
 
         // Launch the transcoding operation.
         mTranscodeStartTime = SystemClock.uptimeMillis();
diff --git a/docs/.gitignore b/docs/.gitignore
deleted file mode 100644
index 928a70a0..00000000
--- a/docs/.gitignore
+++ /dev/null
@@ -1,6 +0,0 @@
-_site
-_pages
-*.sw?
-.sass-cache
-.jekyll-metadata
-Gemfile.lock
diff --git a/docs/Gemfile b/docs/Gemfile
deleted file mode 100644
index 37f5eaa4..00000000
--- a/docs/Gemfile
+++ /dev/null
@@ -1,2 +0,0 @@
-source 'https://rubygems.org'
-gem 'github-pages', group: :jekyll_plugins
diff --git a/docs/README.md b/docs/README.md
index 9312dc35..995fe576 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1 +1 @@
-Read the docs at https://natario1.github.io/Transcoder .
+Read the docs at https://opensource.deepmedia.io/transcoder.
diff --git a/docs/_about/changelog.md b/docs/_about/changelog.md
deleted file mode 100644
index 5a77607d..00000000
--- a/docs/_about/changelog.md
+++ /dev/null
@@ -1,167 +0,0 @@
----
-layout: page
-title: "Changelog"
-order: 3
----
-
-New versions are released through GitHub, so the reference page is the [GitHub Releases](https://github.com/natario1/Transcoder/releases) page.
-
-> Starting from 0.7.0, you can now [support development](https://github.com/sponsors/natario1) through the GitHub Sponsors program. 
-Companies can share a tiny part of their revenue and get private support hours in return. Thanks!
-
-### v0.10.3
-
-- Fix: error when merging many files, thanks to [@DamonChen117][DamonChen117] ([#134][134])
-- Fix: more concatenation issues
-
-<https://github.com/natario1/Transcoder/compare/v0.10.2...v0.10.3>
-
-### v0.10.2
-
-- Fix: error when merging many files ([#132][132])
-
-<https://github.com/natario1/Transcoder/compare/v0.10.1...v0.10.2>
-
-### v0.10.1
-
-- Fix: thumbnails upside down ([#125][125])
-- Fix: clip/trim issues ([#127][127])
-- Fix: seeking issues ([#128][128])
-- Fix: concatenation failure ([#130][130])
-
-<https://github.com/natario1/Transcoder/compare/v0.10.0...v0.10.1>
-
-## v0.10.0
-
-> Transcoder is now distributed through Maven Central. Snapshot releases are available as well.
-
-- New: thumbnails support ([#119][119])
-- Improvement: rewritten transcoding pipeline ([#118][118])
-- Fix: many bugs fixed while rewriting the pipeline ([#118][118])
-
-<https://github.com/natario1/Transcoder/compare/v0.9.1...v0.10.0>
-
-### v0.9.1
-
-- Improvement: `DefaultDataSink` new constructor with support for FileDescriptor. ([#87][87])
-
-<https://github.com/natario1/Transcoder/compare/v0.9.0...v0.9.1>
-
-### v0.9.0
-
-- New: `BlankAudioDataSource` can be used to add muted audio to a video-only track, thanks to [@mudar][mudar] ([#64][64]) 
-- Enhancement: you can now concatenate multiple files even if some of them have no audio, thanks to [@mudar][mudar] ([#64][64]) 
-- Enhancement: you can now concatenate multiple files without audio track, thanks to [@cbernier2][cbernier2] ([#61][61])
-
-<https://github.com/natario1/Transcoder/compare/v0.8.0...v0.9.0>
-
-### v0.8.0
-
-- New: `TrimDataSource` to trim segments. Use it to wrap your original source. Thanks to [@mudar][mudar] ([#50][50])
-- New: `ClipDataSource`, just likes `TrimDataSource` but selects trim values with respect to video start ([#54][54])
-
-> Transcoder will trim video segments only at the closest video sync frame. If your video has few sync
-frames, the trim timestamp might be different than what was selected.
-
-<https://github.com/natario1/Transcoder/compare/v0.7.4...v0.8.0>
-
-##### v0.7.4
-
-- Fix: fixed Xamarin incompatibility, thanks to [@aweck][aweck] ([#41][41])
-- Fix: fixed small bugs with specific API versions / media files ([#47][47])
-- Fix: fixed issues with specific media files, ensure consistent onProgress callback ([#48][48])
-
-<https://github.com/natario1/Transcoder/compare/v0.7.3...v0.7.4>
-
-##### v0.7.3
-
-- Fix: fixed bug with files that do not have an audio track, thanks to [@pawegio][pawegio] ([#31][31])
-- Fix: fixed possible issues with FilePathDataSource ([#32][32])
-
-<https://github.com/natario1/Transcoder/compare/v0.7.2...v0.7.3>
-
-##### v0.7.2
-
-- Improvement: better input format detection. Fixes bugs with certain files ([#29][29])
-- Improvement: added `DefaultAudioStrategy.Builder.bitRate()` option ([#29][29])
-
-<https://github.com/natario1/Transcoder/compare/v0.7.1...v0.7.2>
-
-##### v0.7.1
-
-- Improvement: update the underlying OpenGL library ([#20][20])
-
-<https://github.com/natario1/Transcoder/compare/v0.7.0...v0.7.1>
-
-### v0.7.0
-
-- New: video concatenation to stitch together multiple media ([#14][14])
-- New: select a specific track type (`VIDEO` or `AUDIO`) for sources ([#14][14])
-- New: audio resampling through `DefaultAudioStrategy` ([#16][16])
-- New: custom resampling through `TranscoderOptions.setAudioResampler()` ([#16][16])
-- Breaking change: `TranscoderOptions.setDataSource()` renamed to `addDataSource()` ([#14][14])
-- Breaking change: `TranscoderOptions.setRotation()` renamed to `setVideoRotation()` ([#14][14])
-- Breaking change: `DefaultVideoStrategy.iFrameInterval()` renamed to `keyFrameInterval()` ([#14][14])
-- Breaking change: `DefaultAudioStrategy` now uses a builder - removed old constructor ([#16][16])
-- Improvement: rotate videos through OpenGL instead of using metadata ([#14][14])
-- Improvement: when concatenating multiple sources, automatically clip the longer track (audio or video) ([#17][17])
-- Improvement: various bug fixed ([#18][18])
-
-<https://github.com/natario1/Transcoder/compare/v0.6.0...v0.7.0>
-
-### v0.6.0
-
-- New: ability to change video/audio speed and change each frame timestamp ([#10][10])
-- New: ability to set the video output rotation ([#8][8])
-- Improvement: new frame dropping algorithm, thanks to [@Saqrag][Saqrag] ([#9][9])
-- Improvement: avoid format validation on tracks coming from PassThroughTrackTranscoder, thanks to [@Saqrag][Saqrag] ([#11][11])
-
-<https://github.com/natario1/Transcoder/compare/v0.5.0...v0.6.0>
-
-### v0.5.0
-
-- New: video cropping to any dimension. Encoder will crop the exceeding size. ([#6][6])
-- New: `AspectRatioResizer` to crop to a given aspect ratio. ([#6][6])
-- Breaking change: `MediaTranscoder` renamed to `Transcoder`. ([#6][6])
-- Breaking change: `MediaTranscoderOptions` renamed to `TranscoderOptions`. ([#6][6])
-- Breaking change: `MediaTranscoder.Listener` renamed to `TranscoderListener`. ([#6][6])
-- Improvement: use [EglCore](https://github.com/natario1/EglCore) to replace GL logic. ([#5][5])
-- Improvement: bug fixes and a new demo app to test transcoding options easily ([#4][4])
-
-[Saqrag]: https://github.com/Saqrag
-[pawegio]: https://github.com/pawegio
-[aweck]: https://github.com/aweck
-[mudar]: https://github.com/mudar
-[cbernier2]: https://github.com/cbernier2
-[DamonChen117]: https://github.com/DamonChen117
-
-[4]: https://github.com/natario1/Transcoder/pull/4
-[5]: https://github.com/natario1/Transcoder/pull/5
-[6]: https://github.com/natario1/Transcoder/pull/6
-[8]: https://github.com/natario1/Transcoder/pull/8
-[9]: https://github.com/natario1/Transcoder/pull/9
-[10]: https://github.com/natario1/Transcoder/pull/10
-[14]: https://github.com/natario1/Transcoder/pull/14
-[16]: https://github.com/natario1/Transcoder/pull/16
-[17]: https://github.com/natario1/Transcoder/pull/17
-[18]: https://github.com/natario1/Transcoder/pull/18
-[20]: https://github.com/natario1/Transcoder/pull/20
-[29]: https://github.com/natario1/Transcoder/pull/29
-[31]: https://github.com/natario1/Transcoder/pull/31
-[32]: https://github.com/natario1/Transcoder/pull/32
-[41]: https://github.com/natario1/Transcoder/pull/41
-[47]: https://github.com/natario1/Transcoder/pull/47
-[48]: https://github.com/natario1/Transcoder/pull/48
-[50]: https://github.com/natario1/Transcoder/pull/50
-[54]: https://github.com/natario1/Transcoder/pull/54
-[61]: https://github.com/natario1/Transcoder/pull/61
-[64]: https://github.com/natario1/Transcoder/pull/64
-[87]: https://github.com/natario1/Transcoder/pull/87
-[118]: https://github.com/natario1/Transcoder/pull/118
-[119]: https://github.com/natario1/Transcoder/pull/119
-[125]: https://github.com/natario1/Transcoder/pull/125
-[127]: https://github.com/natario1/Transcoder/pull/127
-[128]: https://github.com/natario1/Transcoder/pull/128
-[130]: https://github.com/natario1/Transcoder/pull/130
-[132]: https://github.com/natario1/Transcoder/pull/132
-[134]: https://github.com/natario1/Transcoder/pull/134
\ No newline at end of file
diff --git a/docs/_about/getting-started.md b/docs/_about/getting-started.md
deleted file mode 100644
index 826130d9..00000000
--- a/docs/_about/getting-started.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-layout: page
-title: "Getting Started"
-description: "Simple guide to transcode your first video"
-order: 2
-disqus: 1
----
-
-### Before you start
-
-If your app targets versions older than API 18, you can override the minSdkVersion by
-adding this line to your manifest file:
-
-```xml
-<uses-sdk tools:overrideLibrary="com.otaliastudios.transcoder" />
-```
-
-In this case you should check at runtime that API level is at least 18, before
-calling any method here.
-
-### Transcoding your first video
-
-Transcoding happens through the `Transcoder` class by passing it an output file path,
-and one of more input data sources. It's pretty simple:
-
-```java
-Transcoder.into(filePath)
-        .addDataSource(context, uri) // or...
-        .addDataSource(filePath) // or...
-        .addDataSource(fileDescriptor) // or...
-        .addDataSource(dataSource)
-        .setListener(new TranscoderListener() {
-             public void onTranscodeProgress(double progress) {}
-             public void onTranscodeCompleted(int successCode) {}
-             public void onTranscodeCanceled() {}
-             public void onTranscodeFailed(@NonNull Throwable exception) {}
-        }).transcode()
-```
-
-However, we offer many APIs and additional features on top that you can read about in the
-in-depth documentation.
-
diff --git a/docs/_about/install.md b/docs/_about/install.md
deleted file mode 100644
index 177524bd..00000000
--- a/docs/_about/install.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-layout: page
-title: "Install"
-description: "Integrate in your project"
-order: 1
----
-
-Transcoder is publicly hosted on the [Maven Central](https://repo1.maven.org/maven2/com/otaliastudios/)
-repository, where you can download the AAR package. To fetch with Gradle, make sure you add the
-Maven Central repository in your root projects `build.gradle` file:
-
-```kotlin
-allprojects {
-  repositories {
-    mavenCentral()
-  }
-}
-```
-
-Then simply download the latest version:
-
-```kotlin
-api("com.otaliastudios:transcoder:{{ site.github_version }}")
-```
-
-> The library works on API 18+, which is the only requirement and should be met by many projects nowadays.
-
-### Snapshots
-
-We deploy snapshots on each push to the main branch. If you want to use the latest, unreleased features,
-you can do so (at your own risk) by adding the snapshot repository:
-
-```kotlin
-allprojects {
-  repositories {
-    maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
-  }
-}
-```
-
-and changing the library version from `{{ site.github_version }}` to `latest-SNAPSHOT`.
\ No newline at end of file
diff --git a/docs/_config.yml b/docs/_config.yml
deleted file mode 100644
index cc597db7..00000000
--- a/docs/_config.yml
+++ /dev/null
@@ -1,45 +0,0 @@
-# Glide: https://github.com/bumptech/glide/blob/gh-pages/_config.yml
-# Source repo: https://github.com/bruth/jekyll-docs-template
-# Source site: http://bruth.github.io/jekyll-docs-template/
-# Ref guide: https://visualstudiomagazine.com/Articles/2015/03/01/GitHub-Pages.aspx?Page=2
-
-# Used by us
-title: Transcoder
-color: '#f8f8f8'
-description: A well documented Android library providing hardware-accelerated video transcoding, using MediaCodec APIs instead of native code (no FFMPEG patent issues). Supports cropping to any dimension, concatenation, clipping, audio processing, video speed and much more. # used by ourselves and by seo tag.
-disqus_shortname: 'natario1-transcoder'
-google_analytics_id: 'UA-155077779-2'
-google_site_verification: '4x49i17ABIrSvUl52SeL0-t0341aTnWWaC62-FYCRT4'
-github: [metadata] # TODO What's this?
-github_repo: Transcoder
-github_version: 0.10.3
-github_branch: main
-baseurl: '/Transcoder' # Keep as an empty string if served up at the root
-collections:
-  about:
-    name: Overview
-    output: true
-  docs:
-    name: Documentation
-    output: true
-  extra:
-    name: More
-    output: true
-screenshots:
-  - 'screenshot-1.png'
-  - 'screenshot-2.png'
-
-# Jekyll specific stuff
-author:
-  name: Mattia Iavarone # Should appear in <head>.
-  email: mat.iavarone@gmail.com
-  github: natario1
-  website: https://natario.dev
-plugins:
-  - jekyll-seo-tag # Add SEO tags
-permalink: /:categories/:title # Ensure permalinks have no date nor extension
-exclude: ['script', 'README.md'] # Exclude non-site files
-highlighter: rouge # Syntax highlighting
-markdown: kramdown # Use the kramdown Markdown renderer
-kramdown:
-  input: GFM # Use Github Flavored Markdown
\ No newline at end of file
diff --git a/docs/_extra/contact.md b/docs/_extra/contact.md
deleted file mode 100644
index 409dfe15..00000000
--- a/docs/_extra/contact.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-layout: page
-title: "Contact"
-order: 3
----
-
-This library is maintained by [Mattia Iavarone](https://github.com/natario1) (@natario1).
-Feel free to contact me privately by <a href="mailto:mat.iavarone@gmail.com">sending an email</a>,
-for support, consulting, or have any other business-related question.
-
-To report issues, please use the [project GitHub page](https://github.com/natario1/Transcoder).
-
diff --git a/docs/_extra/contributing.md b/docs/_extra/contributing.md
deleted file mode 100644
index cf62752b..00000000
--- a/docs/_extra/contributing.md
+++ /dev/null
@@ -1,60 +0,0 @@
----
-layout: page
-title: "Contributing & License"
-order: 1
----
-
-Everyone is welcome to contribute with suggestions or pull requests, as the library is under active development.
-
-We are grateful to anyone who will contribute with fixes, features or feature requests. If you don't
-want to get involved but still want to support the project, please [consider donating](donate).
-
-### Bug reports
-
-Please make sure to fill the bug report issue template on GitHub, if applicable.
-We highly recommend to try to reproduce the bug in the demo app, as this helps a lot in debugging
-and excludes programming errors from your side.
-
-Make sure to include:
-
-- A clear and concise description of what the bug is
-- Transcoder version, device type, Android API level
-- Exact steps to reproduce the issue
-- Description of the expected behavior
-- The original media file(s) that manifest the problem
-
-Recommended extras:
-
-- LogCat logs (use `Logger.setLogLevel(LEVEL_VERBOSE)` to print all)
-- Link to a GitHub repo where the bug is reproducible
-
-### Pull Requests
-
-Please open an issue first!
-
-Unless your PR is a simple fix (typos, documentation, bugs with obvious solution), opening an issue
-will let us discuss the problem, take design decisions and have a reference to the issue description.
-
-If you can, please write tests. We are planning to work on improving the library test coverage soon.
-
-### License
-
-This project is licensed under Apache 2.0. It consists of improvements over
-the [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder)
-project which was licensed under Apache 2.0 as well:
-
-```
-Copyright (C) 2014-2016 Yuya Tanaka
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-```
diff --git a/docs/_extra/donate.md b/docs/_extra/donate.md
deleted file mode 100644
index 3400e886..00000000
--- a/docs/_extra/donate.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-layout: page
-title: "Donate"
-order: 2
----
-
-Transcoder is maintained and, for the most part, developed by Mattia Iavarone ([contact me!](contact)). If you like the project, 
-use it with profit, or simply want to thank back, please consider 
-[sponsoring me](https://github.com/sponsors/natario1) through the GitHub Sponsors program!
-
-I offer private support hours through the sponsorship program and I'm open to help your 
-company with features that are not currently supported by the open source project.
-
-Thank you for any contribution!
-
-
diff --git a/docs/_includes/disqus.html b/docs/_includes/disqus.html
deleted file mode 100644
index b561eef0..00000000
--- a/docs/_includes/disqus.html
+++ /dev/null
@@ -1,12 +0,0 @@
-<div id="disqus_thread"></div>
-<script type="text/javascript">
-    /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
-    var disqus_shortname = '{{ site.disqus_shortname }}'; // required: replace example with your forum shortname
-    /* * * DON'T EDIT BELOW THIS LINE * * */
-    (function() {
-        var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
-        dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
-        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
-    })();
-</script>
-<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
diff --git a/docs/_includes/footer.html b/docs/_includes/footer.html
deleted file mode 100644
index be732e0b..00000000
--- a/docs/_includes/footer.html
+++ /dev/null
@@ -1,4 +0,0 @@
-<footer class="text-center py-3 border-top has-divider">
-    <span>View on <a href="https://github.com/{{ site.author.github }}/{{ site.github_repo }}">GitHub</a> or become <a href="{{ site.baseurl }}/extra/donate">a sponsor</a>!</span>
-    <span class="d-block">Made by <a href="{{ site.author.website }}">{{ site.author.name }}</a></span>
-</footer>
diff --git a/docs/_includes/google_analytics.html b/docs/_includes/google_analytics.html
deleted file mode 100644
index 174548d1..00000000
--- a/docs/_includes/google_analytics.html
+++ /dev/null
@@ -1,7 +0,0 @@
-<script async src="https://www.googletagmanager.com/gtag/js?id={{ site.google_analytics_id }}"></script>
-<script>
-    window.dataLayer = window.dataLayer || [];
-    function gtag(){dataLayer.push(arguments);}
-    gtag('js', new Date());
-    gtag('config', '{{ site.google_analytics_id }}');
-</script>
diff --git a/docs/_includes/head.html b/docs/_includes/head.html
deleted file mode 100644
index d3f33018..00000000
--- a/docs/_includes/head.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<meta charset="utf-8">
-<meta http-equiv="content-type" content="text/html; charset=utf-8">
-<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-
-<meta name="theme-color" content="{{ site.color }}">
-<meta name="msapplication-navbutton-color" content="{{ site.color }}">
-<meta name="msapplication-TileColor" content="{{ site.color }}">
-<meta name="apple-mobile-web-app-capable" content="yes">
-<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
-
-<meta name="description" content="{{ site.description }}">
-<meta name="google-site-verification" content="{{ site.google_site_verification }}" />
-
-<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
-
-{% seo %}
-
-{% if site.google_analytics_id != "" %}
-{% include google_analytics.html %}
-{% endif %}
\ No newline at end of file
diff --git a/docs/_includes/header.html b/docs/_includes/header.html
deleted file mode 100644
index 7e135e05..00000000
--- a/docs/_includes/header.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<header class="text-center py-3 border-bottom has-divider">
-    <!-- center -->
-    <img class="logo align-middle" alt="Logo" src="{{ site.baseurl }}/static/icon_foreground.png"/>
-    <a class="h3 mx-2 align-middle text-decoration-none" href="{{ site.baseurl }}/home">{{ site.title }}</a>
-
-    <!-- left -->
-    <div class="left align-middle">
-        <img class="drawer-toggle p-2 mx-2 d-md-none"
-             alt="Menu toggle"
-             src="{{ site.baseurl }}/icons/menu.svg"
-             onclick="document.getElementById('drawer').classList.toggle('drawer-closed');"/>
-    </div>
-
-    <!-- right -->
-    <div class="right align-middle">
-        <span class="version d-none d-sm-inline-block">latest: v{{ site.github_version }}</span>
-        <a class="p-2 mx-2" href="https://github.com/{{ site.author.github }}/{{ site.github_repo }}">
-            <img alt="GitHub" src="{{ site.baseurl }}/icons/github.svg">
-        </a>
-    </div>
-</header>
diff --git a/docs/_includes/navigation.html b/docs/_includes/navigation.html
deleted file mode 100644
index 366fa27e..00000000
--- a/docs/_includes/navigation.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<ul id="navigation" class="py-4">
-    <li><a href="{{ site.baseurl }}/home">Home</a></li>
-    {% for collection in site.collections %}
-    {% if collection.label != "posts" %}
-    <li class="mt-3"><h5 class="mt-0 mb-1">{{ collection.name }}</h5>
-        <ul>
-            {% assign docs = (collection.docs | sort: "order") %}
-            {% for doc in docs %}
-            <li class="pt-2 pb-2">
-                <a href="{{ site.baseurl }}{{ doc.url }}">{{ doc.title }}</a>
-            </li>
-            {% endfor %}
-        </ul>
-    </li>
-    {% endif %}
-    {% endfor %}
-</ul>
-<script>
-    /* find current item and make it active */
-    const current = document.location.href;
-    const links = document.querySelectorAll('#navigation a');
-    links.forEach((link) => {
-        if (current && link && current.endsWith(link.getAttribute('href'))) {
-            link.parentElement.classList.add('active')
-        }
-    });
-</script>
diff --git a/docs/_layouts/landing.html b/docs/_layouts/landing.html
deleted file mode 100644
index 130ce1fa..00000000
--- a/docs/_layouts/landing.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-    <head>
-        {% include head.html %}
-        <title>{{ site.title }}</title>
-        <link rel="stylesheet" href="{{ site.baseurl }}/css/landing.css">
-        <link rel="stylesheet" href="{{ site.baseurl }}/css/fonts_responsive.css">
-    </head>
-    <body>
-        <div class="container p-5">
-            <img class="d-block mx-auto" id="logo" src="{{ site.baseurl }}/static/icon_foreground.png" alt="Logo">
-            <div class="text-center">
-                <h1 id="title" class="display-2 font-weight-bold mt-2">{{ site.title }}</h1>
-                {{ content }}
-            </div>
-            <div class="text-center">
-                <a class="btn rounded-pill px-3 py-2 mx-2 mt-2" href="{{ site.baseurl }}/home">Documentation</a>
-                <a class="btn rounded-pill px-3 py-2 mx-2 mt-2" href="{{ site.baseurl }}/about/changelog">Changelog</a>
-                <a class="btn rounded-pill px-3 py-2 mx-2 mt-2" href="https://github.com/{{ site.author.github }}/{{ site.github_repo }}">GitHub</a>
-                <a class="btn rounded-pill px-3 py-2 mx-2 mt-2" href="{{ site.baseurl }}/extra/donate">Support</a>
-            </div>
-            <br/>
-            <div class="row">
-                {% assign col = 12 | divided_by: site.screenshots.size %}
-                {% for screenshot in site.screenshots %}
-                <div class="col-12 col-sm-{{ col }} mt-4">
-                    <img class="img-fluid" src="{{ site.baseurl }}/static/{{ screenshot }}" alt="Screenshot">
-                </div>
-                {% endfor %}
-            </div>
-        </div>
-    </body>
-</html>
diff --git a/docs/_layouts/main.html b/docs/_layouts/main.html
deleted file mode 100644
index 6f0bab15..00000000
--- a/docs/_layouts/main.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-    <head>
-        {% include head.html %}
-        <title>{{ site.title }}{% if page.title %} | {{ page.title }}{% endif %}</title>
-        <link rel="stylesheet" href="{{ site.baseurl }}/css/main.css">
-        <link rel="stylesheet" href="{{ site.baseurl }}/css/fonts_responsive.css">
-    </head>
-    <body>
-        {% include header.html %}
-        <div class="container-fluid container-md">
-            <div class="row">
-                <div id="drawer" class="col-md-3 px-4 border-right has-divider drawer drawer-closed">
-                    {% include navigation.html %}
-                </div>
-                <div class="col-12 col-md-9">
-                    <div class="content py-3 px-2">
-                        {{ content }}
-                    </div>
-                    {% if page.disqus == 1 %}
-                    <div class="mt-4">
-                        {% include disqus.html %}
-                    </div>
-                    {% endif %}
-                </div>
-            </div>
-        </div>
-
-        <div class="container-fluid">
-            <div class="row">
-                <div class="col-12 p-0">
-                    {% include footer.html %}
-                </div>
-            </div>
-        </div>
-    </body>
-</html>
diff --git a/docs/_layouts/page.html b/docs/_layouts/page.html
deleted file mode 100644
index e7005d0a..00000000
--- a/docs/_layouts/page.html
+++ /dev/null
@@ -1,30 +0,0 @@
----
-layout: main
----
-
-<div class="page-header border-bottom has-divider pb-3">
-    <h1 class="d-inline-block mt-1 mb-0 mr-2">{{ page.title }}</h1>
-    {% if page.description %}<span class="mr-2">{{ page.description }}</span>{% endif %}
-    <span><a href="https://github.com/{{ site.author.github }}/{{ site.github_repo }}/edit/{{ site.github_branch }}/docs/{{ page.path }}">[edit this page]</a></span>
-</div>
-<div class="page my-4">
-    {{ content }}
-</div>
-<div class="page-footer border-top has-divider">
-    <div class="row text-center">
-        {% assign collection = site.collections | where:"label", page.collection | first %}
-        {% assign next = page.order | plus: 1 %}
-        {% assign previous = page.order | minus: 1 %}
-        {% for doc in collection.docs %}
-        {% if doc.order == previous %}
-        <a class="previous col mt-3" href="{{ site.baseurl }}{{ doc.url }}">< {{ doc.title }}</a>
-        {% endif %}
-        {% endfor %}
-
-        {% for doc in collection.docs %}
-        {% if doc.order == next %}
-        <a class="next col mt-3" href="{{ site.baseurl }}{{ doc.url }}">{{ doc.title }} ></a>
-        {% endif %}
-        {% endfor %}
-    </div>
-</div>
\ No newline at end of file
diff --git a/docs/_docs/advanced-options.md b/docs/advanced-options.mdx
similarity index 73%
rename from docs/_docs/advanced-options.md
rename to docs/advanced-options.mdx
index 0902d23b..0da292d4 100644
--- a/docs/_docs/advanced-options.md
+++ b/docs/advanced-options.mdx
@@ -1,49 +1,46 @@
 ---
-layout: page
-title: "Advanced Options"
-description: "Advanced transcoding options"
-order: 7
-disqus: 1
+title: Advanced Options
 ---
 
-### Video rotation
+# Advanced Options
+
+## Video rotation
 
 You can set the output video rotation with the `setRotation(int)` method. This will apply a clockwise
 rotation to the input video frames. Accepted values are `0`, `90`, `180`, `270`:
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .setVideoRotation(rotation) // 0, 90, 180, 270
-        // ...
+    .setVideoRotation(rotation) // 0, 90, 180, 270
+    // ...
 ```
 
-### Time interpolation
+## Time interpolation
 
 We offer APIs to change the timestamp of each video and audio frame. You can pass a `TimeInterpolator`
 to the transcoder builder to be able to receive the frame timestamp as input, and return a new one
 as output.
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .setTimeInterpolator(timeInterpolator)
-        // ...
+    .setTimeInterpolator(timeInterpolator)
+    // ...
 ```
 
 As an example, this is the implementation of the default interpolator, called `DefaultTimeInterpolator`,
 that will just return the input time unchanged:
 
-```java
-@Override
-public long interpolate(@NonNull TrackType type, long time) {
+```kotlin
+override fun interpolate(type: TrackType, time: Long): Long {
     // Receive input time in microseconds and return a possibly different one.
-    return time;
+    return time
 }
 ```
 
 It should be obvious that returning invalid times can make the process crash at any point, or at least
 the transcoding operation fail.
 
-### Video speed
+## Video speed
 
 We also offer a special time interpolator called `SpeedTimeInterpolator` that accepts a `float` parameter
 and will modify the video speed.
@@ -52,26 +49,26 @@ and will modify the video speed.
 - A speed factor < 1 will slow the video down
 - A speed factor > 1 will accelerate the video
 
-This interpolator can be set using `setTimeInterpolator(TimeInterpolator)`, or, as a shorthand, 
+This interpolator can be set using `setTimeInterpolator(TimeInterpolator)`, or, as a shorthand,
 using `setSpeed(float)`:
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .setSpeed(0.5F) // 0.5x
-        .setSpeed(1F) // Unchanged
-        .setSpeed(2F) // Twice as fast
-        // ...
+    .setSpeed(0.5F) // 0.5x
+    .setSpeed(1F) // Unchanged
+    .setSpeed(2F) // Twice as fast
+    // ...
 ```
 
-### Audio stretching
+## Audio stretching
 
 When a time interpolator alters the frames and samples timestamps, you can either remove audio or
 stretch the audio samples to the new length. This is done through the `AudioStretcher` interface:
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .setAudioStretcher(audioStretcher)
-        // ...
+    .setAudioStretcher(audioStretcher)
+    // ...
 ```
 
 The default audio stretcher, `DefaultAudioStretcher`, will:
@@ -81,19 +78,19 @@ The default audio stretcher, `DefaultAudioStretcher`, will:
 
 Please take a look at the implementation and read class documentation.
 
-### Audio resampling
+## Audio resampling
 
 When a sample rate different than the input is specified (by the `TrackStrategy`, or, when using the
-default audio strategy, by `DefaultAudioStategy.Builder.sampleRate()`), this library will automatically
-perform sample rate conversion for you. 
+default audio strategy, by `DefaultAudioStrategy.Builder.sampleRate()`), this library will automatically
+perform sample rate conversion for you.
 
 This operation is performed by a class called `AudioResampler`. We offer the option to pass your
 own resamplers through the transcoder builder:
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .setAudioResampler(audioResampler)
-        // ...
+    .setAudioResampler(audioResampler)
+    // ...
 ```
 
 The default audio resampler, `DefaultAudioResampler`, will perform both upsampling and downsampling
@@ -101,5 +98,4 @@ with very basic algorithms (drop samples when downsampling, repeat samples when
 Upsampling is generally discouraged - implementing a real upsampling algorithm is probably out of
 the scope of this library.
 
-Please take a look at the implementation and read class documentation.
-
+Please take a look at the implementation and read class documentation.
\ No newline at end of file
diff --git a/docs/changelog.mdx b/docs/changelog.mdx
new file mode 100644
index 00000000..6defa6f1
--- /dev/null
+++ b/docs/changelog.mdx
@@ -0,0 +1,224 @@
+---
+title: Changelog
+---
+
+# Changelog
+
+New versions are released through GitHub, so the reference page is the [GitHub Releases](https://github.com/deepmedia/Transcoder/releases) page.
+
+## 0.11.X
+
+### 0.11.2
+
+- Enhancement: support pass-through audio with more than 2 channels, thanks to [@natario1](https://github.com/natario1) ([#209](https://github.com/deepmedia/Transcoder/pull/209))
+
+[Compare 0.11.1...0.11.2](https://github.com/deepmedia/Transcoder/compare/v0.11.1...v0.11.2).
+
+### 0.11.1
+
+- Fix: add unbounded queue for drifted tracks during initialization, thanks to [@jumperson](https://github.com/jumperson) ([#166](https://github.com/deepmedia/Transcoder/pull/166))
+
+[Compare 0.11.0...0.11.1](https://github.com/deepmedia/Transcoder/compare/v0.11.0...v0.11.1).
+
+### 0.11.0
+
+This release focuses on stability and performance, fixing old bugs that were making the library unstable and unreliable.
+In addition, the library is now available at new maven coordinates `io.deepmedia.community:transcoder-android`.
+The old coordinates will still work and receive updates.
+
+- Enhancement: Revisit transcoding pipeline ([#203](https://github.com/deepmedia/Transcoder/pull/203))
+- Enhancement: Allow SpeedTimeInterpolator reuse, thanks to [@vaibhavpandeyvpz](https://github.com/vaibhavpandeyvpz) ([#199](https://github.com/deepmedia/Transcoder/pull/199))
+- Fix: fix video artifacts, thanks to [@jumperson](https://github.com/jumperson) ([#199](https://github.com/deepmedia/Transcoder/pull/200))
+- Fix: fix end-of-stream flag not sent ([#201](https://github.com/deepmedia/Transcoder/pull/201))
+- Fix: fix performance issues, thanks to [@jumperson](https://github.com/jumperson) ([#202](https://github.com/deepmedia/Transcoder/pull/202))
+- Fix: handle null buffers ([#203](https://github.com/deepmedia/Transcoder/pull/203))
+- Fix: avoid pipeline stalls ([#203](https://github.com/deepmedia/Transcoder/pull/203))
+- Fix: ensure monotonic muxer timestamps ([#203](https://github.com/deepmedia/Transcoder/pull/203))
+
+[Compare 0.10.5...0.11.0](https://github.com/deepmedia/Transcoder/compare/v0.10.5...v0.11.0).
+
+## 0.10.X
+
+### 0.10.5
+
+- Fix: Honor buffer.hasRemaining() and otherwise release the buffer. ([#182][182])
+- Fix: Call progress even when not advanced. ([#170][170])
+- Fix: FilePathDataSource crash before initialize. ([#160][160])
+
+[Compare 0.10.4...0.10.5](https://github.com/deepmedia/Transcoder/compare/v0.10.4...v0.10.5).
+
+### 0.10.4
+
+- Fix: fixed crash in specific conditions ([#140][140])
+- New: AssetFileDescriptorDataSource, can be used to transcode from AssetFileDescriptors ([#140][140])
+
+[Compare 0.10.3...0.10.4](https://github.com/deepmedia/Transcoder/compare/v0.10.3...v0.10.4).
+
+### 0.10.3
+
+- Fix: error when merging many files, thanks to [@DamonChen117][DamonChen117] ([#134][134])
+- Fix: more concatenation issues
+
+[Compare 0.10.2...0.10.3](https://github.com/deepmedia/Transcoder/compare/v0.10.2...v0.10.3).
+
+### 0.10.2
+
+- Fix: error when merging many files ([#132][132])
+
+[Compare 0.10.1...0.10.2](https://github.com/deepmedia/Transcoder/compare/v0.10.1...v0.10.2).
+
+### 0.10.1
+
+- Fix: thumbnails upside down ([#125][125])
+- Fix: clip/trim issues ([#127][127])
+- Fix: seeking issues ([#128][128])
+- Fix: concatenation failure ([#130][130])
+
+[Compare 0.10.0...0.10.1](https://github.com/deepmedia/Transcoder/compare/v0.10.0...v0.10.1).
+
+### 0.10.0
+
+> Transcoder is now distributed through Maven Central. Snapshot releases are available as well.
+
+- New: thumbnails support ([#119][119])
+- Improvement: rewritten transcoding pipeline ([#118][118])
+- Fix: many bugs fixed while rewriting the pipeline ([#118][118])
+
+[Compare 0.9.1...0.10.0](https://github.com/deepmedia/Transcoder/compare/v0.9.1...v0.10.0).
+
+## 0.9.X
+
+### 0.9.1
+
+- Improvement: `DefaultDataSink` new constructor with support for FileDescriptor. ([#87][87])
+
+[Compare 0.9.0...0.9.1](https://github.com/deepmedia/Transcoder/compare/v0.9.0...v0.9.1).
+
+### 0.9.0
+
+- New: `BlankAudioDataSource` can be used to add muted audio to a video-only track, thanks to [@mudar][mudar] ([#64][64])
+- Enhancement: you can now concatenate multiple files even if some of them have no audio, thanks to [@mudar][mudar] ([#64][64])
+- Enhancement: you can now concatenate multiple files without audio track, thanks to [@cbernier2][cbernier2] ([#61][61])
+
+[Compare 0.8.0...0.9.0](https://github.com/deepmedia/Transcoder/compare/v0.8.0...v0.9.0).
+
+## 0.8.X
+
+### 0.8.0
+
+- New: `TrimDataSource` to trim segments. Use it to wrap your original source. Thanks to [@mudar][mudar] ([#50][50])
+- New: `ClipDataSource`, just likes `TrimDataSource` but selects trim values with respect to video start ([#54][54])
+
+> Transcoder will trim video segments only at the closest video sync frame. If your video has few sync
+frames, the trim timestamp might be different than what was selected.
+
+[Compare 0.7.4...0.8.0](https://github.com/deepmedia/Transcoder/compare/v0.7.4...v0.8.0).
+
+## Older versions
+
+### 0.7.4
+
+- Fix: fixed Xamarin incompatibility, thanks to [@aweck][aweck] ([#41][41])
+- Fix: fixed small bugs with specific API versions / media files ([#47][47])
+- Fix: fixed issues with specific media files, ensure consistent onProgress callback ([#48][48])
+
+[Compare 0.7.3...0.7.4](https://github.com/deepmedia/Transcoder/compare/v0.7.3...v0.7.4).
+
+### 0.7.3
+
+- Fix: fixed bug with files that do not have an audio track, thanks to [@pawegio][pawegio] ([#31][31])
+- Fix: fixed possible issues with FilePathDataSource ([#32][32])
+
+[Compare 0.7.2...0.7.3](https://github.com/deepmedia/Transcoder/compare/v0.7.2...v0.7.3).
+
+### 0.7.2
+
+- Improvement: better input format detection. Fixes bugs with certain files ([#29][29])
+- Improvement: added `DefaultAudioStrategy.Builder.bitRate()` option ([#29][29])
+
+[Compare 0.7.1...0.7.2](https://github.com/deepmedia/Transcoder/compare/v0.7.1...v0.7.2).
+
+### 0.7.1
+
+- Improvement: update the underlying OpenGL library ([#20][20])
+
+[Compare 0.7.0...0.7.1](https://github.com/deepmedia/Transcoder/compare/v0.7.0...v0.7.1).
+
+### 0.7.0
+
+- New: video concatenation to stitch together multiple media ([#14][14])
+- New: select a specific track type (`VIDEO` or `AUDIO`) for sources ([#14][14])
+- New: audio resampling through `DefaultAudioStrategy` ([#16][16])
+- New: custom resampling through `TranscoderOptions.setAudioResampler()` ([#16][16])
+- Breaking change: `TranscoderOptions.setDataSource()` renamed to `addDataSource()` ([#14][14])
+- Breaking change: `TranscoderOptions.setRotation()` renamed to `setVideoRotation()` ([#14][14])
+- Breaking change: `DefaultVideoStrategy.iFrameInterval()` renamed to `keyFrameInterval()` ([#14][14])
+- Breaking change: `DefaultAudioStrategy` now uses a builder - removed old constructor ([#16][16])
+- Improvement: rotate videos through OpenGL instead of using metadata ([#14][14])
+- Improvement: when concatenating multiple sources, automatically clip the longer track (audio or video) ([#17][17])
+- Improvement: various bug fixed ([#18][18])
+
+[Compare 0.6.0...0.7.0](https://github.com/deepmedia/Transcoder/compare/v0.6.0...v0.7.0).
+
+### 0.6.0
+
+- New: ability to change video/audio speed and change each frame timestamp ([#10][10])
+- New: ability to set the video output rotation ([#8][8])
+- Improvement: new frame dropping algorithm, thanks to [@Saqrag][Saqrag] ([#9][9])
+- Improvement: avoid format validation on tracks coming from PassThroughTrackTranscoder, thanks to [@Saqrag][Saqrag] ([#11][11])
+
+[Compare 0.5.0...0.6.0](https://github.com/deepmedia/Transcoder/compare/v0.5.0...v0.6.0).
+
+### 0.5.0
+
+- New: video cropping to any dimension. Encoder will crop the exceeding size. ([#6][6])
+- New: `AspectRatioResizer` to crop to a given aspect ratio. ([#6][6])
+- Breaking change: `MediaTranscoder` renamed to `Transcoder`. ([#6][6])
+- Breaking change: `MediaTranscoderOptions` renamed to `TranscoderOptions`. ([#6][6])
+- Breaking change: `MediaTranscoder.Listener` renamed to `TranscoderListener`. ([#6][6])
+- Improvement: use [EglCore](https://github.com/natario1/EglCore) to replace GL logic. ([#5][5])
+- Improvement: bug fixes and a new demo app to test transcoding options easily ([#4][4])
+
+[Saqrag]: https://github.com/Saqrag
+[pawegio]: https://github.com/pawegio
+[aweck]: https://github.com/aweck
+[mudar]: https://github.com/mudar
+[cbernier2]: https://github.com/cbernier2
+[DamonChen117]: https://github.com/DamonChen117
+
+[4]: https://github.com/deepmedia/Transcoder/pull/4
+[5]: https://github.com/deepmedia/Transcoder/pull/5
+[6]: https://github.com/deepmedia/Transcoder/pull/6
+[8]: https://github.com/deepmedia/Transcoder/pull/8
+[9]: https://github.com/deepmedia/Transcoder/pull/9
+[10]: https://github.com/deepmedia/Transcoder/pull/10
+[11]: https://github.com/deepmedia/Transcoder/pull/11
+[14]: https://github.com/deepmedia/Transcoder/pull/14
+[16]: https://github.com/deepmedia/Transcoder/pull/16
+[17]: https://github.com/deepmedia/Transcoder/pull/17
+[18]: https://github.com/deepmedia/Transcoder/pull/18
+[20]: https://github.com/deepmedia/Transcoder/pull/20
+[29]: https://github.com/deepmedia/Transcoder/pull/29
+[31]: https://github.com/deepmedia/Transcoder/pull/31
+[32]: https://github.com/deepmedia/Transcoder/pull/32
+[41]: https://github.com/deepmedia/Transcoder/pull/41
+[47]: https://github.com/deepmedia/Transcoder/pull/47
+[48]: https://github.com/deepmedia/Transcoder/pull/48
+[50]: https://github.com/deepmedia/Transcoder/pull/50
+[54]: https://github.com/deepmedia/Transcoder/pull/54
+[61]: https://github.com/deepmedia/Transcoder/pull/61
+[64]: https://github.com/deepmedia/Transcoder/pull/64
+[87]: https://github.com/deepmedia/Transcoder/pull/87
+[118]: https://github.com/deepmedia/Transcoder/pull/118
+[119]: https://github.com/deepmedia/Transcoder/pull/119
+[125]: https://github.com/deepmedia/Transcoder/pull/125
+[127]: https://github.com/deepmedia/Transcoder/pull/127
+[128]: https://github.com/deepmedia/Transcoder/pull/128
+[130]: https://github.com/deepmedia/Transcoder/pull/130
+[132]: https://github.com/deepmedia/Transcoder/pull/132
+[134]: https://github.com/deepmedia/Transcoder/pull/134
+[140]: https://github.com/deepmedia/Transcoder/pull/140
+[160]: https://github.com/deepmedia/Transcoder/pull/160
+[170]: https://github.com/deepmedia/Transcoder/pull/170
+[182]: https://github.com/deepmedia/Transcoder/pull/182
+
diff --git a/docs/_docs/clipping.md b/docs/clipping.mdx
similarity index 59%
rename from docs/_docs/clipping.md
rename to docs/clipping.mdx
index 783129d1..d2fa03c7 100644
--- a/docs/_docs/clipping.md
+++ b/docs/clipping.mdx
@@ -1,11 +1,9 @@
 ---
-layout: page
-title: "Clipping and trimming"
-description: "How to clip each segment individually on both ends"
-order: 2
-disqus: 1
+title: Clip & Trim
 ---
 
+# Clip & Trim
+
 Starting from `v0.8.0`, Transcoder offers the option to clip or trim video segments, on one or
 both ends. This is done by using special `DataSource` objects that wrap you original source,
 so that, in case of [concatenation](concatenation) of multiple media files, the trimming values
@@ -17,43 +15,42 @@ include video decoding / re-encoding, the clipping position will be moved to the
 This means that the clipped output duration might be different than expected,
 depending on the frequency of sync frames in your original file.
 
-### TrimDataSource
+## TrimDataSource
 
 The `TrimDataSource` class lets you trim segments by specifying the amount of time to be trimmed
-at both ends. For example, the code below will trim the file by 1 second at the beginning, and 
+at both ends. For example, the code below will trim the file by 1 second at the beginning, and
 2 seconds at the end:
 
-```java
-DataSource source = new UriDataSource(context, uri);
-DataSource trim = new TrimDataSource(source, 1000 * 1000, 2 * 1000 * 1000);
+```kotlin
+let source: DataSource = UriDataSource(context, uri)
+let trim: DataSource = TrimDataSource(source, 1000 * 1000, 2 * 1000 * 1000)
 Transcoder.into(filePath)
-        .addDataSource(trim)
-        .transcode()
+    .addDataSource(trim)
+    .transcode()
 ```
 
 It is recommended to always check `source.getDurationUs()` to compute the correct values.
- 
-### ClipDataSource
 
-The `ClipDataSource` class lets you clip segments by specifying a time window. For example, 
+## ClipDataSource
+
+The `ClipDataSource` class lets you clip segments by specifying a time window. For example,
 the code below clip the file from second 1 until second 5:
 
-```java
-DataSource source = new UriDataSource(context, uri);
-DataSource clip = new ClipDataSource(source, 1000 * 1000, 5 * 1000 * 1000);
+```kotlin
+let source: DataSource = UriDataSource(context, uri)
+let clip: DataSource = ClipDataSource(source, 1000 * 1000, 5 * 1000 * 1000)
 Transcoder.into(filePath)
-        .addDataSource(clip)
-        .transcode()
+    .addDataSource(clip)
+    .transcode()
 ```
 
 It is recommended to always check `source.getDurationUs()` to compute the correct values.
- 
-### Related APIs
+
+## Related APIs
 
 |Method|Description|
 |------|-----------|
-|`new TrimDataSource(source, long)`|Creates a new data source trimmed on start.|
-|`new TrimDataSource(source, long, long)`|Creates a new data source trimmed on both ends.|
-|`new ClipDataSource(source, long)`|Creates a new data source clipped on start.|
-|`new ClipDataSource(source, long, long)`|Creates a new data source clipped on both ends.|
-
+|`TrimDataSource(source, long)`|Creates a new data source trimmed on start.|
+|`TrimDataSource(source, long, long)`|Creates a new data source trimmed on both ends.|
+|`ClipDataSource(source, long)`|Creates a new data source clipped on start.|
+|`ClipDataSource(source, long, long)`|Creates a new data source clipped on both ends.|
\ No newline at end of file
diff --git a/docs/_docs/concatenation.md b/docs/concatenation.mdx
similarity index 60%
rename from docs/_docs/concatenation.md
rename to docs/concatenation.mdx
index d311e90a..559cb38d 100644
--- a/docs/_docs/concatenation.md
+++ b/docs/concatenation.mdx
@@ -1,20 +1,20 @@
 ---
-layout: page
-title: "Concatenation"
-description: "How to concatenate video segments"
-order: 3
-disqus: 1
+title: Concatenation
 ---
 
-As you might have guessed from the previous section, you can use `addDataSource(source)` multiple times. 
+# Concatenation
+
+## Appending sources
+
+As you might have guessed from the previous section, you can use `addDataSource(source)` multiple times.
 All the source files will be stitched together:
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .addDataSource(source1)
-        .addDataSource(source2)
-        .addDataSource(source3)
-        // ...
+    .addDataSource(source1)
+    .addDataSource(source2)
+    .addDataSource(source3)
+    // ...
 ```
 
 In the above example, the three videos will be stitched together in the order they are added
@@ -23,25 +23,25 @@ of applying consistent parameters (frame rate, bit rate, sample rate) during the
 
 This is a powerful tool since it can be used per-track:
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .addDataSource(source1) // Audio & Video, 20 seconds
-        .addDataSource(TrackType.VIDEO, source2) // Video, 5 seconds
-        .addDataSource(TrackType.VIDEO, source3) // Video, 5 seconds
-        .addDataSource(TrackType.AUDIO, source4) // Audio, 10 sceonds
-        // ...
+    .addDataSource(source1) // Audio & Video, 20 seconds
+    .addDataSource(TrackType.VIDEO, source2) // Video, 5 seconds
+    .addDataSource(TrackType.VIDEO, source3) // Video, 5 seconds
+    .addDataSource(TrackType.AUDIO, source4) // Audio, 10 sceonds
+    // ...
 ```
 
 In the above example, the output file will be 30 seconds long:
 
 ```
-Video: | •••••••••••••••••• source1 •••••••••••••••••• | •••• source2 •••• | •••• source3 •••• |  
-Audio: | •••••••••••••••••• source1 •••••••••••••••••• | •••••••••••••• source4 •••••••••••••• | 
+Video: | •••••••••••••••••• source1 •••••••••••••••••• | •••• source2 •••• | •••• source3 •••• |
+Audio: | •••••••••••••••••• source1 •••••••••••••••••• | •••••••••••••• source4 •••••••••••••• |
 ```
 
 And that's all you need to do.
 
-### Automatic clipping
+## Automatic clipping
 
 When concatenating data from multiple sources and on different tracks, it's common to have
 a total audio length that is different than the total video length.
@@ -49,13 +49,12 @@ a total audio length that is different than the total video length.
 In this case, `Transcoder` will automatically clip the longest track to match the shorter.
 For example:
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .addDataSource(TrackType.VIDEO, video1) // Video, 30 seconds
-        .addDataSource(TrackType.VIDEO, video2) // Video, 30 seconds
-        .addDataSource(TrackType.AUDIO, music) // Audio, 3 minutes
-        // ...
+    .addDataSource(TrackType.VIDEO, video1) // Video, 30 seconds
+    .addDataSource(TrackType.VIDEO, video2) // Video, 30 seconds
+    .addDataSource(TrackType.AUDIO, music) // Audio, 3 minutes
+    // ...
 ```
 
-In the situation above, we won't use the full music track, but only the first minute of it.
-
+In the situation above, we won't use the full music track, but only the first minute of it.
\ No newline at end of file
diff --git a/docs/css/colors.css b/docs/css/colors.css
deleted file mode 100644
index 4bef97de..00000000
--- a/docs/css/colors.css
+++ /dev/null
@@ -1,65 +0,0 @@
-:root {
-  --color-primary: #204853;
-  --color-primary-active: #102833;
-  --color-primary-hover: #103843;
-  --color-secondary: #030607;
-  --color-accent: #0e95e3;
-  --color-accent-light: #f5fcff;
-  --color-accent-dark: #0e3375;
-  --color-background: #FFFFFF;
-  --color-code: var(--color-primary);
-  --color-code-background: #f8f8f8;
-  --color-text-muted: #6c757d;
-}
-
-body {
-  background-color: var(--color-background);
-}
-
-a {
-  color: var(--color-primary);
-}
-
-code, pre {
-  background-color: var(--color-code-background);
-}
-
-:not(pre) > code {
-  color: var(--color-code);
-}
-
-a:hover {
-  color: var(--color-primary-hover) !important;
-}
-
-.btn-primary {
-    background-color: var(--color-primary) !important;
-    border-color: var(--color-primary) !important;
-    color: white !important;
-}
-
-.btn-primary.active, .btn-primary:active {
-  background-color: var(--color-primary-active) !important;
-  border-color: var(--color-primary-active) !important;
-}
-
-.btn-primary:hover {
-  background-color: var(--color-primary-hover) !important;
-  border-color: var(--color-primary-hover) !important;
-  color: white !important;
-}
-
-.btn-outline-primary {
-    border-color: var(--color-primary) !important;
-}
-
-.btn-outline-primary.active, .btn-outline-primary:active  {
-  background-color: var(--color-primary-active) !important;
-  border-color: var(--color-primary-active) !important;
-}
-
-.btn-outline-primary:hover {
-  border-color: var(--color-primary-hover) !important;
-  background-color: var(--color-primary-hover) !important;
-  color: white !important;
-}
diff --git a/docs/css/fonts.css b/docs/css/fonts.css
deleted file mode 100644
index 8dd13ccb..00000000
--- a/docs/css/fonts.css
+++ /dev/null
@@ -1,33 +0,0 @@
-@import url('https://fonts.googleapis.com/css?family=Lobster+Two:400i,700i|Roboto+Mono|Source+Sans+Pro:400,700&display=swap');
-@import "fonts_responsive.css";
-
-:root {
-  --font-mono: 'Roboto Mono';
-  --font-sans: 'Source Sans Pro';
-  --font-display: 'Lobster Two';
-}
-
-* {
-  font-family: var(--font-sans), sans-serif;
-  font-weight: 400;
-}
-
-h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 {
-  font-family: var(--font-display), cursive;
-  font-style: italic;
-  font-weight: 700 !important;
-}
-
-h4, .h4, h5, .h5, h6, .h6 {
-  font-weight: 400;
-}
-
-button, .btn {
-  font-family: var(--font-display), cursive !important;
-  font-style: italic !important;
-  font-weight: 700 !important;
-}
-
-code, code * {
-  font-family: var(--font-mono) !important;
-}
\ No newline at end of file
diff --git a/docs/css/fonts_responsive.css b/docs/css/fonts_responsive.css
deleted file mode 100644
index 62ecacb3..00000000
--- a/docs/css/fonts_responsive.css
+++ /dev/null
@@ -1,34 +0,0 @@
-/* https://christianoliff.com/blog/bootstrap-with-rfs */
-/* either apply after everything else or add !important here */
-@media (max-width: 1200px) {
-  legend {
-    font-size: calc(1.275rem + 0.3vw);
-  }
-  h1, .h1 {
-    font-size: calc(1.375rem + 1.5vw);
-  }
-  h2, .h2 {
-    font-size: calc(1.325rem + 0.9vw);
-  }
-  h3, .h3 {
-    font-size: calc(1.3rem + 0.6vw);
-  }
-  h4, .h4 {
-    font-size: calc(1.275rem + 0.3vw);
-  }
-  .display-1 {
-    font-size: calc(1.725rem + 5.7vw);
-  }
-  .display-2 {
-    font-size: calc(1.675rem + 5.1vw);
-  }
-  .display-3 {
-    font-size: calc(1.575rem + 3.9vw);
-  }
-  .display-4 {
-    font-size: calc(1.475rem + 2.7vw);
-  }
-  .close {
-    font-size: calc(1.275rem + 0.3vw);
-  }
-}
diff --git a/docs/css/landing.css b/docs/css/landing.css
deleted file mode 100644
index e3da9c69..00000000
--- a/docs/css/landing.css
+++ /dev/null
@@ -1,43 +0,0 @@
-@import "fonts.css";
-@import "colors.css";
-
-:root {
-    --color-gradient-1: var(--color-secondary);
-    --color-gradient-2: var(--color-primary);
-}
-
-html {
-    width: 100%;
-    height: 100%;
-    margin: 0;
-}
-
-body {
-    background: linear-gradient(45deg, var(--color-gradient-1), var(--color-gradient-2)) fixed !important;
-}
-
-#logo {
-    width: 45%;
-    max-width: 340px;
-}
-
-h1 {
-    color: white;
-}
-
-p {
-    color: rgba(255, 255, 255, 0.7);
-    font-size: 1.2em;
-    line-height: 100%;
-}
-
-.btn {
-    color: white !important;
-    background-color: rgba(240, 240, 240, 0.25);
-    font-size: 1.3em;
-}
-
-.btn:hover {
-    color: white !important;
-    background-color: rgba(240, 240, 240, 0.4);
-}
\ No newline at end of file
diff --git a/docs/css/main.css b/docs/css/main.css
deleted file mode 100644
index 6aed50ba..00000000
--- a/docs/css/main.css
+++ /dev/null
@@ -1,291 +0,0 @@
-@import "fonts.css";
-@import "colors.css";
-@import "syntax.css";
-
-:root {
-    --color-footer: var(--color-code-background);
-    --color-table-head: var(--color-code-background);
-    --color-divider: rgba(230, 230, 230, 0.7);
-    --header-height: 65px; /* kind of */
-    --cards-radius: 4px;
-}
-
-html, body {
-    height: 100%;
-}
-
-/* dividers */
-
-.has-divider {
-    border-color: var(--color-divider) !important;
-}
-
-/* header */
-
-header {
-    background: linear-gradient(45deg, var(--color-secondary), var(--color-primary)) fixed;
-    position: fixed;
-    top: 0;
-    width: 100%;
-    z-index: 10;
-}
-
-header .left {
-    position: absolute;
-    left: 0;
-    top: 50%;
-    transform: translateY(-50%);
-}
-
-header .right {
-    position: absolute;
-    right: 0;
-    top: 50%;
-    transform: translateY(-50%);
-}
-
-header a {
-    color: white !important;
-}
-
-header a:hover {
-    color: white !important;
-}
-
-header .logo {
-    height: 32px;
-    width: auto;
-}
-
-header .version {
-    font-size: 0.9em;
-    color: rgba(255, 255, 255, 0.8);
-}
-
-body {
-    /* to offset wrt sticky header */
-    padding-top: var(--header-height);
-}
-
-/* footer */
-
-footer {
-    background-color: var(--color-footer);
-    color: var(--color-text-muted);
-    font-size: 0.9em;
-}
-
-/* drawer */
-
-@media (hover: hover) {
-    .drawer-toggle:hover {
-        background-color: rgba(240, 240, 240, 0.15);
-        border-radius: 50%;
-    }
-}
-
-@media (max-width: 768px) {
-    .drawer {
-        position: fixed;
-        top: 0;
-        left: 0;
-        width: 300px;
-        height: 100%;
-        overflow-x: hidden;
-        overflow-y: auto;
-        background-color: var(--color-background);
-        transition: transform 0.4s cubic-bezier(0.4, 0, 0, 1);
-        z-index: 5;
-        padding-top: var(--header-height);
-    }
-
-    .drawer-closed {
-        transform: translateX(-100%);
-    }
-}
-
-@media (max-width: 480px) {
-    .drawer {
-        width: 100%;
-    }
-}
-
-.drawer ul {
-    list-style: none;
-    margin: 0;
-    padding: 0;
-    line-height: 100%;
-}
-
-.drawer a {
-    color: inherit;
-}
-
-.drawer a:hover {
-    color: var(--color-primary-hover) !important;
-}
-
-.drawer li.active a {
-    color: var(--color-primary);
-    position: relative;
-}
-
-/* .drawer li.active a:hover {
-    color: var(--color-primary-active) !important;
-    text-decoration: none;
-} */
-
-.drawer li.active {
-    position: relative;
-}
-
-.drawer li.active::after {
-    content: '';
-    display: inline-block;
-    position: absolute;
-    right: 0;
-    top: 50%;
-    transform: translateY(-50%);
-    width: 8px;
-    height: 8px;
-    background-color: var(--color-primary);
-    border-radius: 50%;
-}
-
-/* tables */
-
-table {
-    /* same margins that reboot gives to pre */
-    margin-top: 0;
-    margin-bottom: 1rem;
-    border-collapse: collapse;
-    /* make it scrollable if needed */
-    display: block;
-    overflow-x: auto;
-}
-
-thead {
-    background-color: var(--color-table-head);
-}
-
-th {
-    padding: 8px;
-    border: 1px solid var(--color-divider);
-    font-family: var(--font-display), sans-serif;
-    font-weight: 700;
-}
-
-td {
-    padding: 8px;
-    border: 1px solid var(--color-divider);
-}
-
-/* page and content */
-
-.content p {
-    overflow-x: auto; /* for changelog compare links */
-}
-
-.content a, footer a {
-    color: var(--color-accent);
-}
-
-.content a:hover, footer a:hover {
-    color: var(--color-accent) !important;
-}
-
-.content ul {
-    padding-left: 24px;
-}
-
-.content blockquote {
-    background-color: var(--color-code-background);
-    border-radius: var(--cards-radius);
-    border-left: 4px;
-    border-left-style: solid;
-    border-left-color: var(--color-accent);
-    font-size: 0.9em;
-    color: var(--color-text-muted);
-    padding: 0.8rem;
-    /* text-align: justify;
-    position: relative;
-    padding: 0.5rem 32px 0.5rem 0.5rem;
-    text-justify: inter-word; */
-}
-
-/* .content blockquote::after {
-    content: '!';
-    position: absolute;
-    top: 50%;
-    transform: translateY(-50%);
-    right: 16px;
-    font-size: 1rem !important;
-    font-weight: 700 !important;
-    color: var(--color-accent);
-} */
-
-.content blockquote p, .content blockquote ul {
-    margin: 0;
-}
-
-.content h1, .content h2, .content h3, .content h4, .content h5, .content h6 {
-    margin-top: 1.4rem;
-    margin-bottom: 0.8rem;
-}
-
-.page-header span {
-    font-size: 0.9em;
-    color: var(--color-text-muted);
-}
-
-.page-footer a {
-    color: black;
-    font-family: var(--font-display), cursive;
-    font-weight: 700;
-    font-size: 1.2em;
-}
-
-/* code */
-
-pre {
-    border-radius: var(--cards-radius);
-    padding: 0.8rem;
-    font-size: 0.8rem !important;
-    line-height: 1.6;
-}
-
-:not(pre) > code {
-    border-radius: var(--cards-radius);
-    padding: 2px;
-    font-weight: 700 !important;
-    font-size: 0.8rem !important;
-}
-
-.language-java, .language-xml, .language-kotlin, .language-groovy {
-    position: relative;
-}
-
-.language-java::after, .language-xml::after, .language-kotlin::after, .language-groovy::after {
-    position: absolute;
-    top: 0;
-    right: 0;
-    padding: 6px;
-    font-size: 0.65rem;
-    color: var(--color-text-muted);
-}
-
-.language-java::after {
-    content: 'java';
-}
-
-.language-xml::after {
-    content: 'xml';
-}
-
-.language-groovy::after {
-    content: 'groovy';
-}
-
-.language-kotlin::after {
-    content: 'kotlin';
-}
diff --git a/docs/css/syntax.css b/docs/css/syntax.css
deleted file mode 100644
index e41961f4..00000000
--- a/docs/css/syntax.css
+++ /dev/null
@@ -1,86 +0,0 @@
-/* https://github.com/richleland/pygments-css/ */
-@import "colors.css";
-:root {
-    --syntax-muted: #999999;
-    --syntax-annotations: #a49848;
-    --syntax-keyword: #007020;
-    --syntax-operators: #606060;
-    --syntax-numbers: var(--syntax-keyword);
-    --syntax-xml-tags: var(--syntax-keyword);
-}
-
-.highlight .c { color: var(--syntax-muted); font-style: italic } /* Comment */
-.highlight .ch { color: var(--syntax-muted); font-style: italic } /* Comment.Hashbang */
-.highlight .cm { color: var(--syntax-muted); font-style: italic } /* Comment.Multiline */
-.highlight .cp { color: var(--syntax-muted); } /* Comment.Preproc */
-.highlight .cpf { color: var(--syntax-muted); font-style: italic } /* Comment.PreprocFile */
-.highlight .c1 { color: var(--syntax-muted); font-style: italic } /* Comment.Single */
-.highlight .cs { color: var(--syntax-muted); background-color: #fff0f0 } /* Comment.Special */
-
-.highlight .nt { color: var(--syntax-xml-tags); font-weight: bold } /* Name.Tag */
-.highlight .na { color: inherit; /* var(--color-accent) */ } /* Name.Attribute */
-.highlight .nf { color: inherit; /* var(--color-accent) */ } /* Name.Function */
-
-.highlight .mb { color: var(--syntax-numbers) } /* Literal.Number.Bin */
-.highlight .mf { color: var(--syntax-numbers) } /* Literal.Number.Float */
-.highlight .mh { color: var(--syntax-numbers) } /* Literal.Number.Hex */
-.highlight .mi { color: var(--syntax-numbers) } /* Literal.Number.Integer */
-.highlight .mo { color: var(--syntax-numbers) } /* Literal.Number.Oct */
-
-.highlight .nd { color: var(--syntax-annotations); } /* Name.Decorator */
-
-.highlight .k { color: var(--syntax-keyword); font-weight: bold } /* Keyword */
-.highlight .kd { color: var(--syntax-keyword); font-weight: bold } /* Keyword.Declaration */
-.highlight .kt { color: var(--syntax-keyword); font-weight: bold } /* Keyword.Type */
-.highlight .kc { color: var(--syntax-keyword); font-weight: bold } /* Keyword.Constant */
-.highlight .kn { color: var(--syntax-keyword); font-weight: bold } /* Keyword.Namespace */
-.highlight .kp { color: var(--syntax-keyword); font-weight: bold } /* Keyword.Pseudo */
-.highlight .kr { color: var(--syntax-keyword); font-weight: bold } /* Keyword.Reserved */
-
-.highlight .s { color: var(--color-accent-dark); background-color: var(--color-accent-light); } /* Literal.String */
-.highlight .s1 { color: var(--color-accent-dark); background-color: var(--color-accent-light); } /* Literal.String.Single */
-.highlight .sa { color: var(--color-accent-dark) } /* Literal.String.Affix */
-.highlight .sb { color: var(--color-accent-dark) } /* Literal.String.Backtick */
-.highlight .sc { color: var(--color-accent-dark) } /* Literal.String.Char */
-.highlight .dl { color: var(--color-accent-dark) } /* Literal.String.Delimiter */
-.highlight .sd { color: var(--color-accent-dark); font-style: italic } /* Literal.String.Doc */
-.highlight .s2 { color: var(--color-accent-dark) } /* Literal.String.Double */
-.highlight .se { color: var(--color-accent-dark); font-weight: bold } /* Literal.String.Escape */
-.highlight .sh { color: var(--color-accent-dark) } /* Literal.String.Heredoc */
-.highlight .si { color: var(--color-accent-dark); font-style: italic } /* Literal.String.Interpol */
-.highlight .sx { color: var(--color-accent-dark) } /* Literal.String.Other */
-.highlight .sr { color: var(--color-accent-dark) } /* Literal.String.Regex */
-.highlight .ss { color: var(--color-accent-dark) } /* Literal.String.Symbol */
-
-.highlight .o { color: var(--syntax-operators) } /* Operator */
-
-.highlight .hll { background-color: #ffffcc }
-.highlight .err { border: 1px solid #FF0000 } /* Error */
-.highlight .gd { color: #A00000 } /* Generic.Deleted */
-.highlight .ge { font-style: italic } /* Generic.Emph */
-.highlight .gr { color: #FF0000 } /* Generic.Error */
-.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
-.highlight .gi { color: #00A000 } /* Generic.Inserted */
-.highlight .go { color: #888888 } /* Generic.Output */
-.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */
-.highlight .gs { font-weight: bold } /* Generic.Strong */
-.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
-.highlight .gt { color: #0044DD } /* Generic.Traceback */
-.highlight .m { color: #40a070 } /* Literal.Number */
-.highlight .nb { color: #007020 } /* Name.Builtin */
-.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */
-.highlight .no { color: #60add5 } /* Name.Constant */
-.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */
-.highlight .ne { color: #007020 } /* Name.Exception */
-.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */
-.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */
-.highlight .nv { color: #bb60d5 } /* Name.Variable */
-.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */
-.highlight .w { color: #bbbbbb } /* Text.Whitespace */
-.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */
-.highlight .fm { color: #06287e } /* Name.Function.Magic */
-.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */
-.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */
-.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */
-.highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */
-.highlight .il { color: #40a070 } /* Literal.Number.Integer.Long */
\ No newline at end of file
diff --git a/docs/_docs/data-sources.md b/docs/data-sources.mdx
similarity index 50%
rename from docs/_docs/data-sources.md
rename to docs/data-sources.mdx
index 0c31d840..a32b6146 100644
--- a/docs/_docs/data-sources.md
+++ b/docs/data-sources.mdx
@@ -1,48 +1,55 @@
 ---
-layout: page
-title: "Data Sources"
-description: "Sources of media data"
-order: 1
-disqus: 1
+title: Data Sources
 ---
 
+# Data Sources
+
 Starting a transcoding operation will require a source for our data, which is not necessarily
 a `File`. The `DataSource` objects will automatically take care about releasing streams / resources,
 which is convenient but it means that they can not be used twice.
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .addDataSource(source1)
-        .transcode()
+    .addDataSource(source1)
+    .transcode()
 ```
 
-##### UriDataSource
+## Source Types
 
-The Android friendly source can be created with `new UriDataSource(context, uri)` or simply
+#### UriDataSource
+
+The Android friendly source can be created with `UriDataSource(context, uri)` or simply
 using `addDataSource(context, uri)` in the transcoding builder.
 
-##### FileDescriptorDataSource
+#### FileDescriptorDataSource
 
-A data source backed by a file descriptor. Use `new FileDescriptorDataSource(descriptor)` or
-simply `addDataSource(descriptor)` in the transcoding builder.
+A data source backed by a file descriptor. Use `FileDescriptorDataSource(descriptor)` or
+simply `addDataSource(descriptor)` in the transcoding builder. Note that it is the caller
+responsibility to close the file descriptor.
 
-##### FilePathDataSource
+#### FilePathDataSource
 
-A data source backed by a file absolute path. Use `new FilePathDataSource(path)` or
+A data source backed by a file absolute path. Use `FilePathDataSource(path)` or
 simply `addDataSource(path)` in the transcoding builder.
 
-### Track specific sources
+#### AssetFileDescriptorDataSource
+
+A data source backed by Android's AssetFileDescriptor. Use `AssetFileDescriptorDataSource(descriptor)`
+or simply `addDataSource(descriptor)` in the transcoding builder. Note that it is the caller
+responsibility to close the file descriptor.
+
+## Track specific sources
 
 Although a media source can have both audio and video, you can select a specific track
 for transcoding and exclude the other(s). For example, to select the video track only:
- 
+
 ```java
 Transcoder.into(filePath)
-        .addDataSource(TrackType.VIDEO, source)
-        .transcode()
+    .addDataSource(TrackType.VIDEO, source)
+    .transcode()
 ```
- 
-### Related APIs
+
+## Related APIs
 
 |Method|Description|
 |------|-----------|
@@ -50,5 +57,4 @@ Transcoder.into(filePath)
 |`addDataSource(FileDescriptor)`|Adds a new source for the given FileDescriptor.|
 |`addDataSource(String)`|Adds a new source for the given file path.|
 |`addDataSource(DataSource)`|Adds a new source.|
-|`addDataSource(TrackType, DataSource)`|Adds a new source restricted to the given TrackType.|
-
+|`addDataSource(TrackType, DataSource)`|Adds a new source restricted to the given TrackType.|
\ No newline at end of file
diff --git a/docs/_docs/events.md b/docs/events.mdx
similarity index 69%
rename from docs/_docs/events.md
rename to docs/events.mdx
index a2f79b9f..995a7715 100644
--- a/docs/_docs/events.md
+++ b/docs/events.mdx
@@ -1,25 +1,22 @@
 ---
-layout: page
-title: "Transcoding Events"
-description: "Listening to transcoding events"
-order: 4
-disqus: 1
+title: Events
 ---
 
+# Events
 
 Transcoding will happen on a background thread, but we will send updates through the `TranscoderListener`
 interface, which can be applied when building the request:
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .setListenerHandler(handler)
-        .setListener(new TranscoderListener() {
-             public void onTranscodeProgress(double progress) {}
-             public void onTranscodeCompleted(int successCode) {}
-             public void onTranscodeCanceled() {}
-             public void onTranscodeFailed(@NonNull Throwable exception) {}
-        })
-        // ...
+    .setListenerHandler(handler)
+    .setListener(object: TranscoderListener {
+         override fun onTranscodeProgress(progress: Double) = Unit
+         override fun onTranscodeCompleted(successCode: Int) = Unit
+         override fun onTranscodeCanceled() = Unit
+         override fun onTranscodeFailed(exception: Throwable) = Unit
+    })
+    // ...
 ```
 
 All of the listener callbacks are called:
@@ -28,19 +25,19 @@ All of the listener callbacks are called:
 - If it has a handler, on the thread that started the `transcode()` call
 - As a last resort, on the UI thread
 
-##### onTranscodeProgress
+### onTranscodeProgress
 
 This simply sends a double indicating the current progress. The value is typically between 0 and 1,
 but can be a negative value to indicate that we are not able to compute progress (yet?).
 
 This is the right place to update a ProgressBar, for example.
 
-##### onTranscodeCanceled
+### onTranscodeCanceled
 
 The transcoding operation was canceled. This can happen when the `Future` returned by `transcode()`
 is cancelled by the user.
 
-##### onTranscodeFailed
+### onTranscodeFailed
 
 This can happen in a number of cases and is typically out of our control. Input options might be
 wrong, write permissions might be missing, codec might be absent, input file might be not supported
@@ -48,7 +45,7 @@ or simply corrupted.
 
 You can take a look at the `Throwable` being passed to know more about the exception.
 
-##### onTranscodeCompleted
+### onTranscodeCompleted
 
 Transcoding operation did succeed. The success code can be:
 
@@ -56,6 +53,3 @@ Transcoding operation did succeed. The success code can be:
 |----|-------|
 |`Transcoder.SUCCESS_TRANSCODED`|Transcoding was executed successfully. Transcoded file was written to the output path.|
 |`Transcoder.SUCCESS_NOT_NEEDED`|Transcoding was not executed because it was considered **not needed** by the `Validator`.|
-
-[Keep reading](validators) to know about `Validator`s.
-
diff --git a/docs/home.md b/docs/home.md
deleted file mode 100644
index d5ad2786..00000000
--- a/docs/home.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
-layout: main
-title: "Transcoder"
----
-
-# Transcoder
-
-Transcoder is a well documented Android library providing hardware-accelerated video transcoding, 
-using MediaCodec APIs instead of native code (no FFMPEG patent issues). 
-
-<p align="center">
-  <img src="static/banner.png" vspace="10" width="100%">
-</p>
-
-- Fast transcoding to AAC/AVC
-- Hardware accelerated
-- Multithreaded
-- Convenient, fluent API
-- Concatenate multiple video and audio tracks [[docs]](docs/concatenation)
-- Clip or trim video segments [[docs]](docs/clipping)
-- Choose output size, with automatic cropping [[docs]](docs/track-strategies#video-size)
-- Choose output rotation [[docs]](docs/advanced-options#video-rotation) 
-- Choose output speed [[docs]](docs/advanced-options#video-speed)
-- Choose output frame rate [[docs]](docs/track-strategies#other-options)
-- Choose output audio channels [[docs]](docs/track-strategies#audio-strategies)
-- Choose output audio sample rate [[docs]](docs/track-strategies#audio-strategies)
-- Override frames timestamp, e.g. to slow down the middle part of the video [[docs]](docs/advanced-options#time-interpolation) 
-- Error handling [[docs]](docs/events)
-- Configurable validators to e.g. avoid transcoding if the source is already compressed enough [[docs]](docs/validators)
-- Configurable video and audio strategies [[docs]](docs/track-strategies)
-
-### Get started
-
-Get started with [install info](about/install), [quick setup](about/getting-started), or
-start reading the in-depth [documentation](docs/data-sources).
-
-### Notes
-
-This project started as a fork of [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder).
-With respect to the source project, which misses most of the functionality listed above, 
-we have also fixed a huge number of bugs and are much less conservative when choosing options 
-that might not be supported. The source project will always throw - for example, accepting only 16:9,
-AVC Baseline Profile videos - we prefer to try and let the codec fail if it wants to.
-
-### Support
-
-If you like the project, use it with profit, and want to thank back, please consider [donating or
-becoming a supporter](extra/donate).
-
diff --git a/docs/icons/github.svg b/docs/icons/github.svg
deleted file mode 100644
index c8a122a7..00000000
--- a/docs/icons/github.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg"
-     width="24"
-     height="24"
-     viewBox="0 0 512 499.36">
-    <path
-            fill="#fff"
-            fill-rule="evenodd"
-            fill-opacity="0.8"
-            d="M256 0C114.64 0 0 114.61 0 256c0 113.09 73.34 209 175.08 242.9 12.8 2.35 17.47-5.56 17.47-12.34 0-6.08-.22-22.18-.35-43.54-71.2 15.49-86.2-34.34-86.2-34.34-11.64-29.57-28.42-37.45-28.42-37.45-23.27-15.84 1.73-15.55 1.73-15.55 25.69 1.81 39.21 26.38 39.21 26.38 22.84 39.12 59.92 27.82 74.5 21.27 2.33-16.54 8.94-27.82 16.25-34.22-56.84-6.43-116.6-28.43-116.6-126.49 0-27.95 10-50.8 26.35-68.69-2.63-6.48-11.42-32.5 2.51-67.75 0 0 21.49-6.88 70.4 26.24a242.65 242.65 0 0 1 128.18 0c48.87-33.13 70.33-26.24 70.33-26.24 14 35.25 5.18 61.27 2.55 67.75 16.41 17.9 26.31 40.75 26.31 68.69 0 98.35-59.85 120-116.88 126.32 9.19 7.9 17.38 23.53 17.38 47.41 0 34.22-.31 61.83-.31 70.23 0 6.85 4.61 14.81 17.6 12.31C438.72 464.97 512 369.08 512 256.02 512 114.62 397.37 0 256 0z"/>
-</svg>
\ No newline at end of file
diff --git a/docs/icons/menu.svg b/docs/icons/menu.svg
deleted file mode 100644
index 45918677..00000000
--- a/docs/icons/menu.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg"
-     width="24"
-     height="24"
-     viewBox="0 0 24 24">
-    <path fill="none" d="M0 0h24v24H0V0z"/>
-    <path fill="#fff" fill-opacity="0.8" d="M4 18h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1zm0-5h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1zM3 7c0 .55.45 1 1 1h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1z"/>
-</svg>
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
deleted file mode 100644
index ecfd1b98..00000000
--- a/docs/index.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-layout: landing
-title: "Transcoder"
----
-
-Accelerated video transcoding on Android using MediaCodec APIs, without native code (no FFMPEG patent issues). Supports cropping to any dimension, concatenation, audio processing and much more.
\ No newline at end of file
diff --git a/docs/index.mdx b/docs/index.mdx
new file mode 100644
index 00000000..fe762288
--- /dev/null
+++ b/docs/index.mdx
@@ -0,0 +1,81 @@
+---
+title: Intro
+docs:
+ - install
+ - changelog
+ - data-sources
+ - clipping
+ - concatenation
+ - events
+ - validators
+ - track-strategies
+ - advanced-options
+---
+
+# Intro
+
+The Transcoder library transcodes and compresses video files into the MP4 format, with audio support, using hardware-accelerated
+Android codecs available on the device. Works on API 19+ and supports the following set of features:
+
+- Fast transcoding to AAC/AVC
+- Hardware accelerated
+- Convenient, fluent API
+- Thumbnails support
+- [Concatenate](concatenation) multiple video and audio tracks
+- [Clip or trim](clipping) video segments
+- Configure [output size](track-strategies#video-size), with automatic cropping
+- Configure [output rotation](advanced-options#video-rotation)
+- Configure [output speed](advanced-options#video-speed)
+- Configure [output frame rate](track-strategies#other-options)
+- Configure [output audio channels](track-strategies#audio-strategies) and sample rate
+- [Override timestamp](advanced-options#time-interpolation) of frames, for example to slow down parts of the video
+- [Error handling](events)
+- Configurable [validators](validators) to e.g. avoid transcoding if the source is already compressed enough
+- Configurable video and audio [strategies](track-strategies)
+
+> This project started as a fork of [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder).
+With respect to the source project, which misses most of the functionality listed above,
+we have also fixed a huge number of bugs and are much less conservative when choosing options
+that might not be supported. The source project will always throw - for example, accepting only 16:9,
+AVC Baseline Profile videos - we prefer to try and let the codec fail if it wants to.
+
+## Minimal example
+
+```kotlin
+Transcoder.into(filePath)
+    .addDataSource(context, uri) // or...
+    .addDataSource(filePath) // or...
+    .addDataSource(fileDescriptor) // or...
+    .addDataSource(dataSource)
+    .setListener(object : TranscoderListener {
+         override fun onTranscodeProgress(progress: Double) = Unit
+         override fun onTranscodeCompleted(successCode: Int) = Unit
+         override fun onTranscodeCanceled() = Unit
+         override fun onTranscodeFailed(exception: Throwable) = Unit
+    }).transcode()
+```
+
+Please keep reading the documentation to learn about [install instructions](install), configuration options and APIs.
+
+
+## License
+
+This project is licensed under Apache 2.0. It consists of improvements over
+the [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder)
+project which was licensed under Apache 2.0 as well:
+
+```
+Copyright (C) 2014-2016 Yuya Tanaka
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
\ No newline at end of file
diff --git a/docs/install.mdx b/docs/install.mdx
new file mode 100644
index 00000000..3086658d
--- /dev/null
+++ b/docs/install.mdx
@@ -0,0 +1,43 @@
+---
+title: Install
+---
+
+# Installation
+
+Transcoder is publicly hosted on the [Maven Central](https://repo1.maven.org/maven2/io/deepmedia/community/)
+repository, where you can download the AAR package. To fetch with Gradle, assuming that `mavenCentral()` is already
+one of your repository sources, simply declare a new dependency:
+
+```kotlin
+dependencies {
+    api("io.deepmedia.community:transcoder-android:LATEST_VERSION")
+
+    // Or use the legacy coordinates:
+    // api("com.otaliastudios:transcoder:LATEST_VERSION")
+}
+```
+
+Replace `LATEST_VERSION` with the latest version number, {version}.
+
+## Snapshots
+
+We regularly push development snapshots of the library at `https://s01.oss.sonatype.org/content/repositories/snapshots/`
+on each push to main. To use snapshots, add the url as a maven repository and depend on `latest-SNAPSHOT`:
+
+```kotlin
+// settings.gradle.kts
+pluginManagement {
+    repositories {
+        gradlePluginPortal()
+        maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/")
+    }
+}
+
+// build.gradle.kts
+dependencies {
+    api("io.deepmedia.community:transcoder-android:latest-SNAPSHOT")
+
+    // Or use the legacy coordinates:
+    // api("com.otaliastudios:transcoder:latest-SNAPSHOT")
+}
+```
\ No newline at end of file
diff --git a/docs/script/launch b/docs/script/launch
deleted file mode 100755
index 080bfe6c..00000000
--- a/docs/script/launch
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env bash
-#
-# Run a local instance of the site.
-bundle exec jekyll serve
diff --git a/docs/static/banner.png b/docs/static/banner.png
deleted file mode 100644
index 017a4bc1..00000000
Binary files a/docs/static/banner.png and /dev/null differ
diff --git a/docs/static/icon_foreground.png b/docs/static/icon_foreground.png
deleted file mode 100644
index a5b60424..00000000
Binary files a/docs/static/icon_foreground.png and /dev/null differ
diff --git a/docs/static/screenshot-1.png b/docs/static/screenshot-1.png
deleted file mode 100644
index fd47e91f..00000000
Binary files a/docs/static/screenshot-1.png and /dev/null differ
diff --git a/docs/static/screenshot-2.png b/docs/static/screenshot-2.png
deleted file mode 100644
index d43c4f47..00000000
Binary files a/docs/static/screenshot-2.png and /dev/null differ
diff --git a/docs/static/sharechat.png b/docs/static/sharechat.png
deleted file mode 100644
index eaf40c5d..00000000
Binary files a/docs/static/sharechat.png and /dev/null differ
diff --git a/docs/_docs/track-strategies.md b/docs/track-strategies.mdx
similarity index 61%
rename from docs/_docs/track-strategies.md
rename to docs/track-strategies.mdx
index b78fbeb0..d9e24716 100644
--- a/docs/_docs/track-strategies.md
+++ b/docs/track-strategies.mdx
@@ -1,95 +1,92 @@
 ---
-layout: page
-title: "Track Strategies"
-description: "Per-track transcoding options"
-order: 6
-disqus: 1
+title: Track Strategies
 ---
 
+# Track Strategies
 
 Track strategies return options for each track (audio or video) for the engine to understand **how**
 and **if** this track should be transcoded, and whether the whole process should be aborted.
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .setVideoTrackStrategy(videoStrategy)
-        .setAudioTrackStrategy(audioStrategy)
-        // ...
+    .setVideoTrackStrategy(videoStrategy)
+    .setAudioTrackStrategy(audioStrategy)
+    // ...
 ```
 
 The point of `TrackStrategy` is to inspect the input `android.media.MediaFormat` and return
 the output `android.media.MediaFormat`, filled with required options.
 
+## Basic Strategies
+
 This library offers track specific strategies that help with audio and video options (see
 [Audio Strategies](#audio-strategies) and [Video Strategies](#video-strategies)).
 In addition, we have a few built-in strategies that can work for both audio and video:
 
-##### PassThroughTrackStrategy
+### PassThroughTrackStrategy
 
 A TrackStrategy that asks the encoder to keep this track as is, by returning the same input
 format. Note that this is risky, as the input track format might not be supported my the MP4 container.
 
 This will set the `TrackStatus` to `TrackStatus.PASS_THROUGH`.
 
-##### RemoveTrackStrategy
+### RemoveTrackStrategy
 
 A TrackStrategy that asks the encoder to remove this track from the output container, by returning null.
 For instance, this can be used as an audio strategy to remove audio from video/audio streams.
 
 This will set the `TrackStatus` to `TrackStatus.REMOVING`.
 
-### Audio Strategies
+## Audio Strategies
 
 The default internal strategy for audio is a `DefaultAudioStrategy`, which converts the
 audio stream to AAC format with the specified number of channels and [sample rate](advanced-options).
 
-```java
-DefaultAudioStrategy strategy = DefaultAudioStrategy.builder()
-        .channels(DefaultAudioStrategy.CHANNELS_AS_INPUT)
-        .channels(1)
-        .channels(2)
-        .sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT)
-        .sampleRate(44100)
-        .sampleRate(30000)
-        .bitRate(DefaultAudioStrategy.BITRATE_UNKNOWN)
-        .bitRate(bitRate)
-        .build();
+```kotlin
+val strategy: DefaultAudioStrategy = DefaultAudioStrategy.builder()
+    .channels(DefaultAudioStrategy.CHANNELS_AS_INPUT)
+    .channels(1)
+    .channels(2)
+    .sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT)
+    .sampleRate(44100)
+    .sampleRate(30000)
+    .bitRate(DefaultAudioStrategy.BITRATE_UNKNOWN)
+    .bitRate(bitRate)
+    .build()
 
 Transcoder.into(filePath)
-        .setAudioTrackStrategy(strategy)
-        // ...
+    .setAudioTrackStrategy(strategy)
+    // ...
 ```
 
 Take a look at the source code to understand how to manage the `android.media.MediaFormat` object.
 
-### Video Strategies
+## Video Strategies
 
 The default internal strategy for video is a `DefaultVideoStrategy`, which converts the
 video stream to AVC format and is very configurable. The class helps in defining an output size.
 If the output size does not match the aspect ratio of the input stream size, `Transcoder` will
 crop part of the input so it matches the final ratio.
 
-##### Video Size
+### Video Size
 
 We provide helpers for common tasks:
 
-```java
-DefaultVideoStrategy strategy;
-
+```kotlin
 // Sets an exact size. If aspect ratio does not match, cropping will take place.
-strategy = DefaultVideoStrategy.exact(1080, 720).build();
+val strategy = DefaultVideoStrategy.exact(1080, 720).build()
 
 // Keeps the aspect ratio, but scales down the input size with the given fraction.
-strategy = DefaultVideoStrategy.fraction(0.5F).build();
+val strategy = DefaultVideoStrategy.fraction(0.5F).build()
 
 // Ensures that each video size is at most the given value - scales down otherwise.
-strategy = DefaultVideoStrategy.atMost(1000).build();
+val strategy = DefaultVideoStrategy.atMost(1000).build()
 
 // Ensures that minor and major dimension are at most the given values - scales down otherwise.
-strategy = DefaultVideoStrategy.atMost(500, 1000).build();
+val strategy = DefaultVideoStrategy.atMost(500, 1000).build()
 ```
 
-In fact, all of these will simply call `new DefaultVideoStrategy.Builder(resizer)` with a special
+In fact, all of these will simply call `DefaultVideoStrategy.Builder(resizer)` with a special
 resizer. We offer handy resizers:
 
 |Name|Description|
@@ -102,39 +99,39 @@ resizer. We offer handy resizers:
 
 You can also group resizers through `MultiResizer`, which applies resizers in chain:
 
-```java
+```kotlin
 // First scales down, then ensures size is at most 1000. Order matters!
-Resizer resizer = new MultiResizer();
-resizer.addResizer(new FractionResizer(0.5F));
-resizer.addResizer(new AtMostResizer(1000));
+val resizer: Resizer = MultiResizer()
+resizer.addResizer(FractionResizer(0.5F))
+resizer.addResizer(AtMostResizer(1000))
 
 // First makes it 16:9, then ensures size is at most 1000. Order matters!
-Resizer resizer = new MultiResizer();
-resizer.addResizer(new AspectRatioResizer(16F / 9F));
-resizer.addResizer(new AtMostResizer(1000));
+val resizer: Resizer = MultiResizer()
+resizer.addResizer(AspectRatioResizer(16F / 9F))
+resizer.addResizer(AtMostResizer(1000))
 ```
 
 This option is already available through the DefaultVideoStrategy builder, so you can do:
 
-```java
-DefaultVideoStrategy strategy = new DefaultVideoStrategy.Builder()
-        .addResizer(new AspectRatioResizer(16F / 9F))
-        .addResizer(new FractionResizer(0.5F))
-        .addResizer(new AtMostResizer(1000))
-        .build();
+```kotlin
+val strategy: DefaultVideoStrategy = DefaultVideoStrategy.Builder()
+    .addResizer(AspectRatioResizer(16F / 9F))
+    .addResizer(FractionResizer(0.5F))
+    .addResizer(AtMostResizer(1000))
+    .build()
 ```
 
-##### Other options
+### Other options
 
 You can configure the `DefaultVideoStrategy` with other options unrelated to the video size:
 
-```java
-DefaultVideoStrategy strategy = new DefaultVideoStrategy.Builder()
-        .bitRate(bitRate)
-        .bitRate(DefaultVideoStrategy.BITRATE_UNKNOWN) // tries to estimate
-        .frameRate(frameRate) // will be capped to the input frameRate
-        .keyFrameInterval(interval) // interval between key-frames in seconds
-        .build();
+```kotlin
+val strategy: DefaultVideoStrategy = DefaultVideoStrategy.Builder()
+    .bitRate(bitRate)
+    .bitRate(DefaultVideoStrategy.BITRATE_UNKNOWN) // tries to estimate
+    .frameRate(frameRate) // will be capped to the input frameRate
+    .keyFrameInterval(interval) // interval between key-frames in seconds
+    .build()
 ```
 
 ### Compatibility
@@ -143,14 +140,14 @@ As stated pretty much everywhere, **not all codecs/devices/manufacturers support
 This is a complex issue which is especially important for video strategies, as a wrong size can lead
 to a transcoding error or corrupted file.
 
-Android platform specifies requirements for manufacturers through the [CTS (Compatibility test suite)](https://source.android.com/compatibility/cts).
+Android platform specifies requirements for manufacturers through the [Compatibility test suite](https://source.android.com/compatibility/cts) (CTS).
 Only a few codecs and sizes are **strictly** required to work.
 
 We collect common presets in the `DefaultVideoStrategies` class:
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .setVideoTrackStrategy(DefaultVideoStrategies.for720x1280()) // 16:9
-        .setVideoTrackStrategy(DefaultVideoStrategies.for360x480()) // 4:3
-        // ...
+    .setVideoTrackStrategy(DefaultVideoStrategies.for720x1280()) // 16:9
+    .setVideoTrackStrategy(DefaultVideoStrategies.for360x480()) // 4:3
+    // ...
 ```
\ No newline at end of file
diff --git a/docs/_docs/validators.md b/docs/validators.mdx
similarity index 88%
rename from docs/_docs/validators.md
rename to docs/validators.mdx
index f28eb3a0..0ee78c4d 100644
--- a/docs/_docs/validators.md
+++ b/docs/validators.mdx
@@ -1,18 +1,16 @@
 ---
-layout: page
-title: "Validators"
-description: "Validate or abort the transcoding process"
-order: 5
-disqus: 1
+title: Validators
 ---
 
+# Validators
+
 Validators tell the engine whether the transcoding process should start or not based on the status
 of the audio and video track.
 
-```java
+```kotlin
 Transcoder.into(filePath)
-        .setValidator(validator)
-        // ...
+    .setValidator(validator)
+    // ...
 ```
 
 This can be used, for example, to:
@@ -35,19 +33,18 @@ The TrackStatus enum contains the following values:
 The `TrackStatus` value depends on the [track strategy](track-strategies) that was used.
 We provide a few validators that can be injected for typical usage.
 
-##### DefaultValidator
+### DefaultValidator
 
 This is the default validator and it returns true when any of the track is `COMPRESSING` or `REMOVING`.
 In the other cases, transcoding is typically not needed so we abort the operation.
 
-##### WriteAlwaysValidator
+### WriteAlwaysValidator
 
 This validator always returns true and as such will always write to target file, no matter the track status,
 presence of tracks and so on. For instance, the output container file might have no tracks.
 
-##### WriteVideoValidator
+### WriteVideoValidator
 
 A Validator that gives priority to the video track. Transcoding will not happen if the video track does not need it,
 even if the audio track might need it. If reducing file size is your only concern, this can avoid compressing
-files that would not benefit so much from compressing the audio track only.
-
+files that would not benefit so much from compressing the audio track only.
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index d97d827a..4cd7d5bf 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,23 +1,6 @@
-# Project-wide Gradle settings.
 
-# IDE (e.g. Android Studio) users:
-# Settings specified in this file will override any Gradle settings
-# configured through the IDE.
-
-# For more details on how to configure your build environment visit
-# http://www.gradle.org/docs/current/userguide/build_environment.html
-
-# Specifies the JVM arguments used for the daemon process.
-# The setting is particularly useful for tweaking memory settings.
-# Default value: -Xmx10248m -XX:MaxPermSize=256m
-# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
-
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
-
-android.enableJetifier=true
 android.useAndroidX=true
-org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m
-# ^ https://github.com/Kotlin/dokka/issues/1405
\ No newline at end of file
+org.gradle.caching=true
+org.gradle.caching.debug=false
+org.gradle.parallel=true
+org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e4908f08..254dc618 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
diff --git a/lib-legacy/.gitignore b/lib-legacy/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/lib-legacy/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/lib-legacy/build.gradle.kts b/lib-legacy/build.gradle.kts
new file mode 100644
index 00000000..e7b9a926
--- /dev/null
+++ b/lib-legacy/build.gradle.kts
@@ -0,0 +1,77 @@
+import io.deepmedia.tools.deployer.model.Secret
+
+plugins {
+    id("com.android.library")
+    id("io.deepmedia.tools.deployer")
+}
+
+android {
+    namespace = "com.otaliastudios.transcoder"
+    compileSdk = 34
+    defaultConfig.minSdk = 21
+    publishing { singleVariant("release") }
+}
+
+dependencies {
+    api(project(":lib"))
+}
+
+deployer {
+    content {
+        component {
+            fromSoftwareComponent("release")
+            emptyDocs()
+            emptySources()
+        }
+    }
+
+    projectInfo {
+        groupId = "com.otaliastudios"
+        artifactId = "transcoder"
+        release.version = "0.11.2" // change :lib and README
+        description = "Accelerated video compression and transcoding on Android using MediaCodec APIs (no FFMPEG/LGPL licensing issues). Supports cropping to any dimension, concatenation, audio processing and much more."
+        url = "https://opensource.deepmedia.io/transcoder"
+        scm.fromGithub("deepmedia", "Transcoder")
+        license(apache2)
+        developer("Mattia Iavarone", "mattia@deepmedia.io", "DeepMedia", "https://deepmedia.io")
+    }
+
+    signing {
+        key = secret("SIGNING_KEY")
+        password = secret("SIGNING_PASSWORD")
+    }
+
+    // use "deployLocal" to deploy to local maven repository
+    localSpec {
+        directory.set(rootProject.layout.buildDirectory.get().dir("inspect"))
+        signing {
+            key = absent()
+            password = absent()
+        }
+    }
+
+    // use "deployNexus" to deploy to OSSRH / maven central
+    nexusSpec {
+        auth.user = secret("SONATYPE_USER")
+        auth.password = secret("SONATYPE_PASSWORD")
+        syncToMavenCentral = true
+    }
+
+    // use "deployNexusSnapshot" to deploy to sonatype snapshots repo
+    nexusSpec("snapshot") {
+        auth.user = secret("SONATYPE_USER")
+        auth.password = secret("SONATYPE_PASSWORD")
+        repositoryUrl = ossrhSnapshots1
+        release.version = "latest-SNAPSHOT"
+    }
+
+    // use "deployGithub" to deploy to github packages
+    githubSpec {
+        repository = "Transcoder"
+        owner = "deepmedia"
+        auth {
+            user = secret("GHUB_USER")
+            token = secret("GHUB_PERSONAL_ACCESS_TOKEN")
+        }
+    }
+}
diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index a670c667..ecf39815 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -1,63 +1,112 @@
-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.deployer.model.Secret
+import org.gradle.api.publish.maven.internal.publication.MavenPublicationInternal
 
 plugins {
     id("com.android.library")
-    id("kotlin-android")
-    id("io.deepmedia.tools.publisher")
+    kotlin("android")
+    id("io.deepmedia.tools.deployer")
+    id("org.jetbrains.dokka") version "1.9.20"
 }
 
 android {
-    setCompileSdkVersion(property("compileSdkVersion") as Int)
+    namespace = "io.deepmedia.transcoder"
+    compileSdk = 34
     defaultConfig {
-        setMinSdkVersion(property("minSdkVersion") as Int)
-        setTargetSdkVersion(property("targetSdkVersion") as Int)
-        versionCode = 1
-        versionName = "0.10.3"
+        minSdk = 21
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
     }
-    buildTypes["release"].isMinifyEnabled = false
+    testOptions {
+        targetSdk = 23
+    }
+    publishing {
+        singleVariant("release")
+    }
 }
 
+kotlin {
+    jvmToolchain(17)
+}
 
 dependencies {
-    api("com.otaliastudios.opengl:egloo:0.6.0")
-    api("androidx.annotation:annotation:1.1.0")
+    api("com.otaliastudios.opengl:egloo:0.6.1")
+    api("androidx.annotation:annotation:1.8.2")
 
-    androidTestImplementation("androidx.test:runner:1.3.0")
-    androidTestImplementation("androidx.test:rules:1.3.0")
-    androidTestImplementation("androidx.test.ext:junit:1.1.2")
+    androidTestImplementation("androidx.test:runner:1.6.1")
+    androidTestImplementation("androidx.test:rules:1.6.1")
+    androidTestImplementation("androidx.test.ext:junit:1.2.1")
     androidTestImplementation("org.mockito:mockito-android:2.28.2")
+
+    dokkaPlugin("org.jetbrains.dokka:android-documentation-plugin:1.9.20")
+}
+
+val javadocs = tasks.register<Jar>("dokkaJavadocJar") {
+    dependsOn(tasks.dokkaJavadoc)
+    from(tasks.dokkaJavadoc.flatMap { it.outputDirectory })
+    archiveClassifier.set("javadoc")
+}
+
+// Ugly workaround because the snapshot publication has different version and maven-publish
+// is then unable to determine the right coordinates for lib-legacy dependency on this project
+publishing.publications.withType<MavenPublicationInternal>().configureEach {
+    isAlias = name != "localReleaseComponent"
 }
 
-publisher {
-    project.description = "Accelerated video transcoding using Android MediaCodec API without native code (no LGPL/patent issues)."
-    project.artifact = "transcoder"
-    project.group = "com.otaliastudios"
-    project.url = "https://github.com/natario1/Transcoder"
-    project.scm = GithubScm("natario1", "Transcoder")
-    project.addLicense(License.APACHE_2_0)
-    project.addDeveloper("natario1", "mat.iavarone@gmail.com")
-    release.sources = Release.SOURCES_AUTO
-    release.docs = Release.DOCS_AUTO
+deployer {
+    content {
+        component {
+            fromSoftwareComponent("release")
+            kotlinSources()
+            docs(javadocs)
+        }
+    }
+
+    projectInfo {
+        groupId = "io.deepmedia.community"
+        artifactId = "transcoder-android"
+        release.version = "0.11.2" // change :lib-legacy and README
+        description = "Accelerated video compression and transcoding on Android using MediaCodec APIs (no FFMPEG/LGPL licensing issues). Supports cropping to any dimension, concatenation, audio processing and much more."
+        url = "https://opensource.deepmedia.io/transcoder"
+        scm.fromGithub("deepmedia", "Transcoder")
+        license(apache2)
+        developer("Mattia Iavarone", "mattia@deepmedia.io", "DeepMedia", "https://deepmedia.io")
+    }
+
+    signing {
+        key = secret("SIGNING_KEY")
+        password = secret("SIGNING_PASSWORD")
+    }
 
-    directory()
+    // use "deployLocal" to deploy to local maven repository
+    localSpec {
+        directory.set(rootProject.layout.buildDirectory.get().dir("inspect"))
+        signing {
+            key = absent()
+            password = absent()
+        }
+    }
 
-    sonatype {
-        auth.user = "SONATYPE_USER"
-        auth.password = "SONATYPE_PASSWORD"
-        signing.key = "SIGNING_KEY"
-        signing.password = "SIGNING_PASSWORD"
+    // use "deployNexus" to deploy to OSSRH / maven central
+    nexusSpec {
+        auth.user = secret("SONATYPE_USER")
+        auth.password = secret("SONATYPE_PASSWORD")
+        syncToMavenCentral = true
     }
 
-    sonatype("snapshot") {
-        repository = Sonatype.OSSRH_SNAPSHOT_1
+    // use "deployNexusSnapshot" to deploy to sonatype snapshots repo
+    nexusSpec("snapshot") {
+        auth.user = secret("SONATYPE_USER")
+        auth.password = secret("SONATYPE_PASSWORD")
+        repositoryUrl = ossrhSnapshots1
         release.version = "latest-SNAPSHOT"
-        auth.user = "SONATYPE_USER"
-        auth.password = "SONATYPE_PASSWORD"
-        signing.key = "SIGNING_KEY"
-        signing.password = "SIGNING_PASSWORD"
+    }
+
+    // use "deployGithub" to deploy to github packages
+    githubSpec {
+        repository = "Transcoder"
+        owner = "deepmedia"
+        auth {
+            user = secret("GHUB_USER")
+            token = secret("GHUB_PERSONAL_ACCESS_TOKEN")
+        }
     }
 }
diff --git a/lib/src/androidTest/assets/issue_102/sample.mp4 b/lib/src/androidTest/assets/issue_102/sample.mp4
new file mode 100644
index 00000000..8ce533f4
Binary files /dev/null and b/lib/src/androidTest/assets/issue_102/sample.mp4 differ
diff --git a/lib/src/androidTest/assets/issue_137/0.amr b/lib/src/androidTest/assets/issue_137/0.amr
new file mode 100644
index 00000000..f03133a1
Binary files /dev/null and b/lib/src/androidTest/assets/issue_137/0.amr differ
diff --git a/lib/src/androidTest/assets/issue_137/1.amr b/lib/src/androidTest/assets/issue_137/1.amr
new file mode 100644
index 00000000..34ca5bb5
Binary files /dev/null and b/lib/src/androidTest/assets/issue_137/1.amr differ
diff --git a/lib/src/androidTest/assets/issue_137/2.amr b/lib/src/androidTest/assets/issue_137/2.amr
new file mode 100644
index 00000000..509c7a9d
Binary files /dev/null and b/lib/src/androidTest/assets/issue_137/2.amr differ
diff --git a/lib/src/androidTest/assets/issue_137/3.amr b/lib/src/androidTest/assets/issue_137/3.amr
new file mode 100644
index 00000000..7c228f52
Binary files /dev/null and b/lib/src/androidTest/assets/issue_137/3.amr differ
diff --git a/lib/src/androidTest/assets/issue_137/4.amr b/lib/src/androidTest/assets/issue_137/4.amr
new file mode 100644
index 00000000..1e3a4729
Binary files /dev/null and b/lib/src/androidTest/assets/issue_137/4.amr differ
diff --git a/lib/src/androidTest/assets/issue_137/5.amr b/lib/src/androidTest/assets/issue_137/5.amr
new file mode 100644
index 00000000..81935c94
Binary files /dev/null and b/lib/src/androidTest/assets/issue_137/5.amr differ
diff --git a/lib/src/androidTest/assets/issue_137/6.amr b/lib/src/androidTest/assets/issue_137/6.amr
new file mode 100644
index 00000000..4eea56e2
Binary files /dev/null and b/lib/src/androidTest/assets/issue_137/6.amr differ
diff --git a/lib/src/androidTest/assets/issue_137/7.amr b/lib/src/androidTest/assets/issue_137/7.amr
new file mode 100644
index 00000000..da09937e
Binary files /dev/null and b/lib/src/androidTest/assets/issue_137/7.amr differ
diff --git a/lib/src/androidTest/assets/issue_137/8.amr b/lib/src/androidTest/assets/issue_137/8.amr
new file mode 100644
index 00000000..79609eb6
Binary files /dev/null and b/lib/src/androidTest/assets/issue_137/8.amr differ
diff --git a/lib/src/androidTest/assets/issue_137/main.mp3 b/lib/src/androidTest/assets/issue_137/main.mp3
new file mode 100644
index 00000000..4f7ee306
Binary files /dev/null and b/lib/src/androidTest/assets/issue_137/main.mp3 differ
diff --git a/lib/src/androidTest/assets/issue_180/party.mp4 b/lib/src/androidTest/assets/issue_180/party.mp4
new file mode 100644
index 00000000..a8c03b6f
Binary files /dev/null and b/lib/src/androidTest/assets/issue_180/party.mp4 differ
diff --git a/lib/src/androidTest/assets/issue_184/transcode.3gp b/lib/src/androidTest/assets/issue_184/transcode.3gp
new file mode 100644
index 00000000..71a06092
Binary files /dev/null and b/lib/src/androidTest/assets/issue_184/transcode.3gp differ
diff --git a/lib/src/androidTest/assets/issue_75/bbb_720p_30mb.mp4 b/lib/src/androidTest/assets/issue_75/bbb_720p_30mb.mp4
new file mode 100644
index 00000000..f29424ee
Binary files /dev/null and b/lib/src/androidTest/assets/issue_75/bbb_720p_30mb.mp4 differ
diff --git a/lib/src/androidTest/java/com/otaliastudios/transcoder/integration/IssuesTests.kt b/lib/src/androidTest/java/com/otaliastudios/transcoder/integration/IssuesTests.kt
new file mode 100644
index 00000000..1ed497a8
--- /dev/null
+++ b/lib/src/androidTest/java/com/otaliastudios/transcoder/integration/IssuesTests.kt
@@ -0,0 +1,154 @@
+package com.otaliastudios.transcoder.integration
+
+import android.media.MediaFormat
+import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
+import android.media.MediaMetadataRetriever
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.otaliastudios.transcoder.Transcoder
+import com.otaliastudios.transcoder.TranscoderListener
+import com.otaliastudios.transcoder.TranscoderOptions
+import com.otaliastudios.transcoder.common.TrackType
+import com.otaliastudios.transcoder.internal.utils.Logger
+import com.otaliastudios.transcoder.source.AssetFileDescriptorDataSource
+import com.otaliastudios.transcoder.source.BlankAudioDataSource
+import com.otaliastudios.transcoder.source.ClipDataSource
+import com.otaliastudios.transcoder.source.FileDescriptorDataSource
+import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy
+import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
+import com.otaliastudios.transcoder.validator.WriteAlwaysValidator
+import org.junit.Assume
+import org.junit.AssumptionViolatedException
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+
+@RunWith(AndroidJUnit4::class)
+class IssuesTests {
+
+    class Helper(val issue: Int) {
+
+        val log = Logger("Issue$issue")
+        val context = InstrumentationRegistry.getInstrumentation().context
+
+        fun output(
+            name: String = System.currentTimeMillis().toString(),
+            extension: String = "mp4"
+        ) = File(context.cacheDir, "$name.$extension").also { it.parentFile!!.mkdirs() }
+
+        fun input(filename: String) = AssetFileDescriptorDataSource(
+            context.assets.openFd("issue_$issue/$filename")
+        )
+
+        fun transcode(
+            output: File = output(),
+            assertTranscoded: Boolean = true,
+            assertDuration: Boolean = true,
+            builder: TranscoderOptions.Builder.() -> Unit,
+        ): File = runCatching {
+            Logger.setLogLevel(Logger.LEVEL_VERBOSE)
+            val transcoder = Transcoder.into(output.absolutePath)
+            transcoder.apply(builder)
+            transcoder.setListener(object : TranscoderListener {
+                override fun onTranscodeCanceled() = Unit
+                override fun onTranscodeCompleted(successCode: Int) {
+                    if (assertTranscoded) {
+                        require(successCode == Transcoder.SUCCESS_TRANSCODED)
+                    }
+                }
+                override fun onTranscodeFailed(exception: Throwable) = Unit
+                override fun onTranscodeProgress(progress: Double) = Unit
+            })
+            transcoder.transcode().get()
+            if (assertDuration) {
+                val retriever = MediaMetadataRetriever()
+                retriever.setDataSource(output.absolutePath)
+                val duration = retriever.extractMetadata(METADATA_KEY_DURATION)!!.toLong()
+                log.e("Video duration is $duration")
+                assert(duration > 0L)
+                retriever.release()
+            }
+            return output
+        }.getOrElse {
+            if (it.toString().contains("c2.android.avc.encoder was unable to create the input surface (1x1)")) {
+                log.w("Hit known emulator bug. Skipping the test.")
+                throw AssumptionViolatedException("Hit known emulator bug.")
+            }
+            throw it
+        }
+    }
+
+
+    @Test(timeout = 16000)
+    fun issue137() = with(Helper(137)) {
+        transcode {
+            addDataSource(ClipDataSource(input("main.mp3"), 0L, 1000_000L))
+            addDataSource(input("0.amr"))
+            addDataSource(ClipDataSource(input("main.mp3"), 2000_000L, 3000_000L))
+            addDataSource(input("1.amr"))
+            addDataSource(ClipDataSource(input("main.mp3"), 4000_000L, 5000_000L))
+            addDataSource(input("2.amr"))
+            addDataSource(ClipDataSource(input("main.mp3"), 6000_000L, 7000_000L))
+            addDataSource(input("3.amr"))
+            addDataSource(ClipDataSource(input("main.mp3"), 8000_000L, 9000_000L))
+            addDataSource(input("4.amr"))
+            addDataSource(ClipDataSource(input("main.mp3"), 10000_000L, 11000_000L))
+            addDataSource(input("5.amr"))
+            addDataSource(ClipDataSource(input("main.mp3"), 12000_000L, 13000_000L))
+            addDataSource(input("6.amr"))
+            addDataSource(ClipDataSource(input("main.mp3"), 14000_000L, 15000_000L))
+            addDataSource(input("7.amr"))
+            addDataSource(ClipDataSource(input("main.mp3"), 16000_000L, 17000_000L))
+            addDataSource(input("8.amr"))
+        }
+        Unit
+    }
+
+    @Test(timeout = 16000)
+    fun issue184() = with(Helper(184)) {
+        transcode {
+            addDataSource(TrackType.VIDEO, input("transcode.3gp"))
+            setVideoTrackStrategy(DefaultVideoStrategy.exact(400, 400).build())
+        }
+        Unit
+    }
+
+    @Test(timeout = 16000)
+    fun issue102() = with(Helper(102)) {
+        transcode {
+            addDataSource(input("sample.mp4"))
+            setValidator(WriteAlwaysValidator())
+        }
+        Unit
+    }
+
+    @Test(timeout = 16000)
+    fun issue180() = with(Helper(180)) {
+        transcode {
+            val vds = input("party.mp4")
+            val duration = run {
+                vds.initialize()
+                vds.durationUs.also { vds.deinitialize() }
+            }
+            check(duration > 0L) { "Invalid duration: $duration" }
+            addDataSource(TrackType.VIDEO, vds)
+            addDataSource(TrackType.AUDIO, BlankAudioDataSource(duration))
+        }
+        Unit
+    }
+
+    @Test(timeout = 16000)
+    fun issue75_workaround() = with(Helper(75)) {
+        transcode {
+            val vds = input("bbb_720p_30mb.mp4")
+            addDataSource(ClipDataSource(vds, 0, 500_000))
+            setVideoTrackStrategy(DefaultVideoStrategy.exact(300, 300).build())
+            // API 23:
+            // This video seems to have wrong number of channels in metadata (6) wrt MediaCodec decoder output (2)
+            // so when using DefaultAudioStrategy.CHANNELS_AS_INPUT we target 6 and try to upscale from 2 to 6, which fails
+            // The workaround below explicitly sets a number of channels different than CHANNELS_AS_INPUT to make it work
+            // setAudioTrackStrategy(DefaultAudioStrategy.builder().channels(1).build())
+        }
+        Unit
+    }
+}
\ No newline at end of file
diff --git a/lib/src/androidTest/java/com/otaliastudios/transcoder/engine/internal/ISO6709LocationParserTest.java b/lib/src/androidTest/java/com/otaliastudios/transcoder/internal/utils/ISO6709LocationParserTest.java
similarity index 92%
rename from lib/src/androidTest/java/com/otaliastudios/transcoder/engine/internal/ISO6709LocationParserTest.java
rename to lib/src/androidTest/java/com/otaliastudios/transcoder/internal/utils/ISO6709LocationParserTest.java
index b955322c..ef3a64a2 100644
--- a/lib/src/androidTest/java/com/otaliastudios/transcoder/engine/internal/ISO6709LocationParserTest.java
+++ b/lib/src/androidTest/java/com/otaliastudios/transcoder/internal/utils/ISO6709LocationParserTest.java
@@ -1,10 +1,8 @@
-package com.otaliastudios.transcoder.engine.internal;
+package com.otaliastudios.transcoder.internal.utils;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.otaliastudios.transcoder.internal.utils.ISO6709LocationParser;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml
index f62d05a4..48652056 100644
--- a/lib/src/main/AndroidManifest.xml
+++ b/lib/src/main/AndroidManifest.xml
@@ -1,6 +1,3 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.otaliastudios.transcoder">
-
+<manifest>
     <application />
-
 </manifest>
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java b/lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java
index 42b35679..c8a47b7a 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.content.res.AssetFileDescriptor;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Handler;
@@ -11,6 +12,7 @@
 import com.otaliastudios.transcoder.resample.DefaultAudioResampler;
 import com.otaliastudios.transcoder.sink.DataSink;
 import com.otaliastudios.transcoder.sink.DefaultDataSink;
+import com.otaliastudios.transcoder.source.AssetFileDescriptorDataSource;
 import com.otaliastudios.transcoder.source.DataSource;
 import com.otaliastudios.transcoder.source.FileDescriptorDataSource;
 import com.otaliastudios.transcoder.source.FilePathDataSource;
@@ -162,37 +164,43 @@ public Builder addDataSource(@NonNull TrackType type, @NonNull DataSource dataSo
         }
 
         @NonNull
-        @SuppressWarnings("unused")
         public Builder addDataSource(@NonNull FileDescriptor fileDescriptor) {
             return addDataSource(new FileDescriptorDataSource(fileDescriptor));
         }
 
         @NonNull
-        @SuppressWarnings("unused")
         public Builder addDataSource(@NonNull TrackType type, @NonNull FileDescriptor fileDescriptor) {
             return addDataSource(type, new FileDescriptorDataSource(fileDescriptor));
         }
 
         @NonNull
-        @SuppressWarnings("unused")
+        public Builder addDataSource(@NonNull AssetFileDescriptor assetFileDescriptor) {
+            return addDataSource(new AssetFileDescriptorDataSource(assetFileDescriptor));
+        }
+
+        @NonNull
+        public Builder addDataSource(@NonNull TrackType type, @NonNull AssetFileDescriptor assetFileDescriptor) {
+            return addDataSource(type, new AssetFileDescriptorDataSource(assetFileDescriptor));
+        }
+
+        @NonNull
         public Builder addDataSource(@NonNull String inPath) {
             return addDataSource(new FilePathDataSource(inPath));
         }
 
         @NonNull
-        @SuppressWarnings("unused")
         public Builder addDataSource(@NonNull TrackType type, @NonNull String inPath) {
             return addDataSource(type, new FilePathDataSource(inPath));
         }
 
         @NonNull
-        @SuppressWarnings({"unused", "UnusedReturnValue"})
+        @SuppressWarnings({"UnusedReturnValue"})
         public Builder addDataSource(@NonNull Context context, @NonNull Uri uri) {
             return addDataSource(new UriDataSource(context, uri));
         }
 
         @NonNull
-        @SuppressWarnings({"unused", "UnusedReturnValue"})
+        @SuppressWarnings({"UnusedReturnValue"})
         public Builder addDataSource(@NonNull TrackType type, @NonNull Context context, @NonNull Uri uri) {
             return addDataSource(type, new UriDataSource(context, uri));
         }
@@ -205,7 +213,6 @@ public Builder addDataSource(@NonNull TrackType type, @NonNull Context context,
          * @return this for chaining
          */
         @NonNull
-        @SuppressWarnings("unused")
         public Builder setAudioTrackStrategy(@Nullable TrackStrategy trackStrategy) {
             this.audioTrackStrategy = trackStrategy;
             return this;
@@ -219,7 +226,6 @@ public Builder setAudioTrackStrategy(@Nullable TrackStrategy trackStrategy) {
          * @return this for chaining
          */
         @NonNull
-        @SuppressWarnings("unused")
         public Builder setVideoTrackStrategy(@Nullable TrackStrategy trackStrategy) {
             this.videoTrackStrategy = trackStrategy;
             return this;
@@ -255,7 +261,6 @@ public Builder setListenerHandler(@Nullable Handler listenerHandler) {
          * @return this for chaining
          */
         @NonNull
-        @SuppressWarnings("unused")
         public Builder setValidator(@Nullable Validator validator) {
             this.validator = validator;
             return this;
@@ -269,7 +274,6 @@ public Builder setValidator(@Nullable Validator validator) {
          * @return this for chaining
          */
         @NonNull
-        @SuppressWarnings("unused")
         public Builder setVideoRotation(int rotation) {
             this.videoRotation = rotation;
             return this;
@@ -299,7 +303,6 @@ public Builder setTimeInterpolator(@NonNull TimeInterpolator timeInterpolator) {
          * @return this for chaining
          */
         @NonNull
-        @SuppressWarnings("unused")
         public Builder setSpeed(float speedFactor) {
             return setTimeInterpolator(new SpeedTimeInterpolator(speedFactor));
         }
@@ -313,7 +316,6 @@ public Builder setSpeed(float speedFactor) {
          * @return this for chaining
          */
         @NonNull
-        @SuppressWarnings("unused")
         public Builder setAudioStretcher(@NonNull AudioStretcher audioStretcher) {
             this.audioStretcher = audioStretcher;
             return this;
@@ -328,7 +330,6 @@ public Builder setAudioStretcher(@NonNull AudioStretcher audioStretcher) {
          * @return this for chaining
          */
         @NonNull
-        @SuppressWarnings("unused")
         public Builder setAudioResampler(@NonNull AudioResampler audioResampler) {
             this.audioResampler = audioResampler;
             return this;
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..8fb4e1cc 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/common/TrackType.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/common/TrackType.kt
@@ -2,8 +2,9 @@ package com.otaliastudios.transcoder.common
 
 import android.media.MediaFormat
 
-enum class TrackType {
-    AUDIO, VIDEO
+enum class TrackType(internal val displayName: String) {
+    AUDIO("Audio"), VIDEO("Video");
+
 }
 
 internal val MediaFormat.trackType get() = requireNotNull(trackTypeOrNull) {
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..f3c9724a 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Codecs.kt
@@ -1,16 +1,18 @@
 package com.otaliastudios.transcoder.internal
 
 import android.media.MediaCodec
+import android.media.MediaCodecList
 import android.media.MediaFormat
-import android.view.Surface
+import android.opengl.EGL14
+import com.otaliastudios.opengl.core.EglCore
+import com.otaliastudios.opengl.surface.EglWindowSurface
 import com.otaliastudios.transcoder.common.TrackStatus
 import com.otaliastudios.transcoder.common.TrackType
-import com.otaliastudios.transcoder.internal.media.MediaFormatProvider
+import com.otaliastudios.transcoder.internal.media.MediaFormatConstants
 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
+import java.nio.ByteBuffer
+import kotlin.properties.Delegates.observable
 
 /**
  * Encoders are shared between segments. This is not strictly needed but it is more efficient
@@ -25,9 +27,52 @@ internal class Codecs(
         private val current: TrackMap<Int>
 ) {
 
+    class Surface(
+        private val context: EglCore,
+        val window: EglWindowSurface,
+    ) {
+        fun release() {
+            window.release()
+            context.release()
+        }
+    }
+
+    class Codec(val codec: MediaCodec, val surface: Surface? = null, var log: Logger? = null) {
+        var dequeuedInputs by observable(0) { _, _, _ -> log?.v(state) }
+        var dequeuedOutputs by observable(0) { _, _, _ -> log?.v(state) }
+        val state get(): String = "dequeuedInputs=$dequeuedInputs dequeuedOutputs=$dequeuedOutputs heldInputs=${heldInputs.size}"
+
+        private val heldInputs = ArrayDeque<Pair<ByteBuffer, Int>>()
+
+        fun getInputBuffer(): Pair<ByteBuffer, Int>? {
+            if (heldInputs.isNotEmpty()) {
+                return heldInputs.removeFirst().also { log?.v(state) }
+            }
+            val id = codec.dequeueInputBuffer(100)
+            return if (id >= 0) {
+                dequeuedInputs++
+                val buf = checkNotNull(codec.getInputBuffer(id)) { "inputBuffer($id) should not be null." }
+                buf to id
+            } else {
+                log?.i("buffer() failed with $id. $state")
+                null
+            }
+        }
+
+        /**
+         * When we're not ready to write into this buffer, it can be held for later.
+         * Previously we were returning it to the codec with timestamp=0, flags=0, but especially
+         * on older Android versions that can create subtle issues.
+         * It's better to just keep the buffer here and reuse it on the next [getInputBuffer] call.
+         */
+        fun holdInputBuffer(buffer: ByteBuffer, id: Int) {
+            heldInputs.addLast(buffer to id)
+        }
+    }
+
     private val log = Logger("Codecs")
 
-    val encoders = object : TrackMap<Pair<MediaCodec, Surface?>> {
+    val encoders = object : TrackMap<Codec> {
 
         override fun has(type: TrackType) = tracks.all[type] == TrackStatus.COMPRESSING
 
@@ -35,14 +80,45 @@ internal class Codecs(
             val format = tracks.outputFormats.audio
             val codec = MediaCodec.createEncoderByType(format.getString(MediaFormat.KEY_MIME)!!)
             codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
-            codec to null
+            Codec(codec, null)
         }
 
         private val lazyVideo by lazy {
             val format = tracks.outputFormats.video
+            val width = format.getInteger(MediaFormat.KEY_WIDTH)
+            val height = format.getInteger(MediaFormat.KEY_HEIGHT)
+            log.i("Destination video surface size: ${width}x${height} @ ${format.getInteger(MediaFormatConstants.KEY_ROTATION_DEGREES)}")
+            log.i("Destination video format: $format")
+
+            val allCodecs = MediaCodecList(MediaCodecList.REGULAR_CODECS)
+            val videoEncoders = allCodecs.codecInfos.filter { it.isEncoder && it.supportedTypes.any { it.startsWith("video/") } }
+            log.i("Available encoders: ${videoEncoders.joinToString { "${it.name} (${it.supportedTypes.joinToString()})" }}")
+
+            // Could consider MediaCodecList(MediaCodecList.REGULAR_CODECS).findEncoderForFormat(format)
+            // But it's trickier, for example, format should not include frame rate on API 21 and maybe other quirks.
             val codec = MediaCodec.createEncoderByType(format.getString(MediaFormat.KEY_MIME)!!)
             codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
-            codec to codec.createInputSurface()
+            log.i("Selected encoder ${codec.name}")
+            val surface = codec.createInputSurface()
+
+            log.i("Creating OpenGL context on ${Thread.currentThread()} (${surface.isValid})")
+            val eglContext = EglCore(EGL14.EGL_NO_CONTEXT, EglCore.FLAG_RECORDABLE)
+            val eglWindow = EglWindowSurface(eglContext, surface, true)
+            eglWindow.makeCurrent()
+
+            // On API28 (possibly others) emulator, this happens. If we don't throw early, it fails later with unclear
+            // errors - a tombstone dump saying that src.width() & 1 == 0 (basically, complains that surface size is odd)
+            // and an error much later on during encoder's dequeue. Surface size is odd because it's 1x1.
+            val (eglWidth, eglHeight) = eglWindow.getWidth() to eglWindow.getHeight()
+            if (eglWidth != width || eglHeight != height) {
+                log.e("OpenGL surface has wrong size (expected: ${width}x${height}, found: ${eglWindow.getWidth()}x${eglWindow.getHeight()}).")
+                // Throw a clear error in this very specific scenario so we can catch it in tests.
+                if (codec.name == "c2.android.avc.encoder" && eglWidth == 1 && eglHeight == 1) {
+                    error("c2.android.avc.encoder was unable to create the input surface (1x1).")
+                }
+            }
+
+            Codec(codec, Surface(eglContext, eglWindow))
         }
 
         override fun get(type: TrackType) = when (type) {
@@ -63,7 +139,7 @@ internal class Codecs(
 
     fun release() {
         encoders.forEach {
-            it.first.release()
+            it.surface?.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 cb441d7b..4201a56c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt
@@ -21,18 +21,30 @@ internal class DataSources private constructor(
 
     private fun DataSource.init() = if (!isInitialized) initialize() else Unit
     private fun DataSource.deinit() = if (isInitialized) deinitialize() else Unit
-    private fun List<DataSource>.init() = forEach { it.init() }
-    private fun List<DataSource>.deinit() = forEach { it.deinit() }
+    private fun List<DataSource>.init() = forEach {
+        log.i("initializing $it... (isInit=${it.isInitialized})")
+        it.init()
+    }
+    private fun List<DataSource>.deinit() = forEach {
+        log.i("deinitializing $it... (isInit=${it.isInitialized})")
+        it.deinit()
+    }
 
     init {
+        log.i("initializing videoSources...")
         videoSources.init()
+        log.i("initializing audioSources...")
         audioSources.init()
     }
 
+    // Save and deinit on release, because a source that is discarded for video
+    // might be active for audio. We don't want to deinit right away.
+    private val discarded = mutableListOf<DataSource>()
+
     private val videoSources: List<DataSource> = run {
         val valid = videoSources.count { it.getTrackFormat(TrackType.VIDEO) != null }
         when (valid) {
-            0 -> listOf<DataSource>().also { videoSources.deinit() }
+            0 -> listOf<DataSource>().also { discarded += videoSources }
             videoSources.size -> videoSources
             else -> videoSources // Tracks will crash
         }
@@ -40,14 +52,17 @@ internal class DataSources private constructor(
 
     private val audioSources: List<DataSource> = run {
         val valid = audioSources.count { it.getTrackFormat(TrackType.AUDIO) != null }
+        log.i("computing audioSources, valid=$valid")
         when (valid) {
-            0 -> listOf<DataSource>().also { audioSources.deinit() }
+            0 -> listOf<DataSource>().also { discarded += audioSources }
             audioSources.size -> audioSources
             else -> {
                 // Some tracks do not have audio, while some do. Replace with BlankAudio.
                 audioSources.map { source ->
                     if (source.getTrackFormat(TrackType.AUDIO) != null) source
-                    else BlankAudioDataSource(source.durationUs).also { source.deinit() }
+                    else BlankAudioDataSource(source.durationUs).also {
+                        discarded += source
+                    }
                 }
             }
         }
@@ -64,8 +79,9 @@ internal class DataSources private constructor(
 
     fun release() {
         log.i("release(): releasing...")
-        video.forEach { it.deinit() }
-        audio.forEach { it.deinit() }
+        video.deinit()
+        audio.deinit()
+        discarded.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 b8884169..74adba3e 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt
@@ -3,7 +3,6 @@ package com.otaliastudios.transcoder.internal
 import com.otaliastudios.transcoder.common.TrackType
 import com.otaliastudios.transcoder.internal.pipeline.Pipeline
 import com.otaliastudios.transcoder.internal.pipeline.State
-import com.otaliastudios.transcoder.internal.utils.Logger
 
 internal class Segment(
         val type: TrackType,
@@ -11,7 +10,7 @@ internal class Segment(
         private val pipeline: Pipeline,
 ) {
 
-    private val log = Logger("Segment($type,$index)")
+    // private val log = Logger("Segment($type,$index)")
     private var state: State<Unit>? = null
 
     fun advance(): Boolean {
@@ -20,11 +19,18 @@ internal class Segment(
     }
 
     fun canAdvance(): Boolean {
-        log.v("canAdvance(): state=$state")
+        // log.v("canAdvance(): state=$state")
         return state == null || state !is State.Eos
     }
 
+    fun needsSleep(): Boolean {
+        when(val s = state ?: return false) {
+            is State.Ok -> return false
+            is State.Failure -> return s.sleep
+        }
+    }
+
     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 bdcc31a1..fc666547 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Segments.kt
@@ -9,9 +9,9 @@ import com.otaliastudios.transcoder.internal.utils.TrackMap
 import com.otaliastudios.transcoder.internal.utils.mutableTrackMapOf
 
 internal 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, Int, TrackStatus, MediaFormat) -> Pipeline
 ) {
 
     private val log = Logger("Segments")
@@ -21,7 +21,7 @@ internal 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
@@ -85,15 +85,16 @@ internal class Segments(
         // who check it during pipeline init.
         currentIndex[type] = index
         val pipeline = factory(
-                type,
-                index,
-                tracks.all[type],
-                tracks.outputFormats[type]
+            type,
+            index,
+            sources[type].size,
+            tracks.all[type],
+            tracks.outputFormats[type]
         )
         return Segment(
-                type = type,
-                index = index,
-                pipeline = pipeline
+            type = type,
+            index = index,
+            pipeline = pipeline
         ).also {
             current[type] = it
         }
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..f3030e7f 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Timer.kt
@@ -7,10 +7,10 @@ import com.otaliastudios.transcoder.source.DataSource
 import com.otaliastudios.transcoder.time.TimeInterpolator
 
 internal 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")
@@ -55,7 +55,7 @@ internal class Timer(
         }
     }
 
-    private val interpolators = mutableMapOf<Pair<TrackType, Int>, TimeInterpolator>()
+    private val interpolators = mutableMapOf<Pair<TrackType, Int>, SegmentInterpolator>()
 
     fun localize(type: TrackType, index: Int, positionUs: Long): Long? {
         if (!tracks.active.has(type)) return null
@@ -68,27 +68,40 @@ internal class Timer(
         return localizedUs
     }
 
-    fun interpolator(type: TrackType, index: Int) = interpolators.getOrPut(type to index) {
-        object : TimeInterpolator {
+    fun interpolator(type: TrackType, index: Int): SegmentInterpolator = interpolators.getOrPut(type to index) {
+        SegmentInterpolator(
+            log = Logger("${type.displayName}Interpolator$index/${sources[type].size}"),
+            user = interpolator,
+            previous = if (index == 0) null else interpolator(type, index - 1)
+        )
+    }
+
+    class SegmentInterpolator(
+        private val log: Logger,
+        private val user: TimeInterpolator,
+        previous: SegmentInterpolator?,
+    ) : TimeInterpolator {
 
-            private var lastOut = 0L
-            private var firstIn = Long.MAX_VALUE
-            private val firstOut = when {
-                index == 0 -> 0L
-                else -> {
-                    // Add 10 just so they're not identical.
-                    val previous = interpolators[type to index - 1]!!
-                    previous.interpolate(type, Long.MAX_VALUE) + 10L
-                }
+        private var inputBase = Long.MIN_VALUE
+        private var interpolatedLast = Long.MIN_VALUE
+        private var outputLast = Long.MIN_VALUE
+        private val outputBase by lazy {
+            when (previous) {
+                null -> 0L
+                // Not interpolated by user, so we give user interpolator a consistent stream.
+                // Add a bit of distance just so they're not identical, won't be noticeable.
+                else -> previous.outputLast + 1L
+            }.also {
+                log.i("Found output base timestamp: $it")
             }
+        }
 
-            override fun interpolate(type: TrackType, time: Long) = when (time) {
-                Long.MAX_VALUE -> lastOut
-                else -> {
-                    if (firstIn == Long.MAX_VALUE) firstIn = time
-                    lastOut = firstOut + (time - firstIn)
-                    interpolator.interpolate(type, lastOut)
-                }
+        override fun interpolate(type: TrackType, time: Long): Long {
+            if (inputBase == Long.MIN_VALUE) inputBase = time
+            outputLast = outputBase + (time - inputBase)
+            return user.interpolate(type, outputLast).also {
+                check(it > interpolatedLast) { "Timestamps must be monotonically increasing: $it, $interpolatedLast" }
+                interpolatedLast = it
             }
         }
     }
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..d091e8c6 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt
@@ -57,6 +57,7 @@ internal class Tracks(
             strategy: TrackStrategy,
             sources: List<DataSource>? // null or not-empty
     ): Pair<MediaFormat, TrackStatus> {
+        log.i("resolveTrack($type), sources=${sources?.size}, strategy=${strategy::class.simpleName}")
         if (sources == null) {
             return MediaFormat() to TrackStatus.ABSENT
         }
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..33e03c5d 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
@@ -6,11 +6,8 @@ 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.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
 import kotlin.math.ceil
 import kotlin.math.floor
 
@@ -19,15 +16,10 @@ import kotlin.math.floor
  * remixing, stretching. TODO: With some extra work this could be split in different steps.
  */
 internal class AudioEngine(
-        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)
-    }
-    private val log = Logger("AudioEngine(${ID.getAndIncrement()})")
+    private val stretcher: AudioStretcher,
+    private val resampler: AudioResampler,
+    private val targetFormat: MediaFormat
+): QueuedStep<DecoderData, DecoderChannel, EncoderData, EncoderChannel>("AudioEngine"), DecoderChannel {
 
     override val channel = this
     private val buffers = ShortBuffers()
@@ -35,8 +27,9 @@ internal class AudioEngine(
     private val MediaFormat.sampleRate get() = getInteger(KEY_SAMPLE_RATE)
     private val MediaFormat.channels get() = getInteger(KEY_CHANNEL_COUNT)
 
+    private val chunks = ChunkQueue(log)
+    private var readyToDrain = false
     private lateinit var rawFormat: MediaFormat
-    private lateinit var chunks: ChunkQueue
     private lateinit var remixer: AudioRemixer
 
     override fun handleSourceFormat(sourceFormat: MediaFormat): Surface? = null
@@ -44,35 +37,40 @@ internal class AudioEngine(
     override fun handleRawFormat(rawFormat: MediaFormat) {
         log.i("handleRawFormat($rawFormat)")
         this.rawFormat = rawFormat
-        remixer = AudioRemixer[rawFormat.channels, targetFormat.channels]
-        chunks = ChunkQueue(rawFormat.sampleRate, rawFormat.channels)
+        this.remixer = AudioRemixer[rawFormat.channels, targetFormat.channels]
+        this.readyToDrain = true
     }
 
     override fun enqueueEos(data: DecoderData) {
-        log.i("enqueueEos()")
+        log.i("enqueueEos (${chunks.size} in queue)")
         data.release(false)
         chunks.enqueueEos()
     }
 
     override fun enqueue(data: DecoderData) {
         val stretch = (data as? DecoderTimerData)?.timeStretch ?: 1.0
-        chunks.enqueue(data.buffer.asShortBuffer(), data.timeUs, stretch) {
-            data.release(false)
-        }
+        chunks.enqueue(data.buffer.asShortBuffer(), data.timeUs, stretch) { data.release(false) }
     }
 
     override fun drain(): State<EncoderData> {
+        if (!readyToDrain) {
+            log.i("drain(): not ready, waiting... (${chunks.size} in queue)")
+            return State.Retry(false)
+        }
         if (chunks.isEmpty()) {
+            // nothing was enqueued
             log.i("drain(): no chunks, waiting...")
-            return State.Wait
+            return State.Retry(false)
         }
         val (outBytes, outId) = next.buffer() ?: return run {
-            log.i("drain(): no next buffer, waiting...")
-            State.Wait
+            // dequeueInputBuffer failed
+            log.i("drain(): no next buffer, waiting... (${chunks.size} in queue)")
+            State.Retry(true)
         }
         val outBuffer = outBytes.asShortBuffer()
         return chunks.drain(
-                eos = State.Eos(EncoderData(outBytes, outId, 0))
+            eos = State.Eos(EncoderData(outBytes, outId, 0)),
+            format = rawFormat
         ) { inBuffer, timeUs, stretch ->
             val outSize = outBuffer.remaining()
             val inSize = inBuffer.remaining()
@@ -103,16 +101,18 @@ internal 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.
             outBytes.clear()
             outBytes.limit(outBuffer.limit() * BYTES_PER_SHORT)
             outBytes.position(outBuffer.position() * BYTES_PER_SHORT)
+            log.v("drain(): passing buffer $outId to encoder... ${chunks.size} in queue")
             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 52eaa357..cf8008a3 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,10 @@
 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 com.otaliastudios.transcoder.internal.utils.Logger
+import java.nio.ByteBuffer
 import java.nio.ShortBuffer
 
 private data class Chunk(
@@ -19,21 +24,33 @@ 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) {
+internal class ChunkQueue(private val log: Logger) {
     private val queue = ArrayDeque<Chunk>()
+    private val pool = ShortBufferPool()
 
     fun isEmpty() = queue.isEmpty()
+    val size get() = queue.size
 
     fun enqueue(buffer: ShortBuffer, timeUs: Long, timeStretch: Double, release: () -> Unit) {
-        require(buffer.hasRemaining())
-        queue.addLast(Chunk(buffer, timeUs, timeStretch, release))
+        if (buffer.hasRemaining()) {
+            if (queue.size >= 3) {
+                val copy = pool.take(buffer)
+                queue.addLast(Chunk(copy, timeUs, timeStretch, { pool.give(copy) }))
+                release()
+            } else {
+                queue.addLast(Chunk(buffer, timeUs, timeStretch, release))
+            }
+        } else {
+            log.w("enqueued invalid buffer ($timeUs, ${buffer.capacity()})")
+            release()
+        }
     }
 
     fun enqueueEos() {
         queue.addLast(Chunk.Eos)
     }
 
-    fun <T> drain(eos: T, action: (buffer: ShortBuffer, timeUs: Long, timeStretch: Double) -> T): T {
+    fun <T> drain(format: MediaFormat, eos: T, action: (buffer: ShortBuffer, timeUs: Long, timeStretch: Double) -> T): T {
         val head = queue.removeFirst()
         if (head === Chunk.Eos) return eos
 
@@ -43,14 +60,48 @@ internal class ChunkQueue(private val sampleRate: Int, private val channels: Int
         // Action can reduce the limit for any reason. Restore it before comparing sizes.
         head.buffer.limit(limit)
         if (head.buffer.hasRemaining()) {
+            // We could technically hold onto the same chunk, but in practice it's better to
+            // release input buffers back to the decoder otherwise it can get stuck
             val consumed = size - head.buffer.remaining()
+            val sampleRate = format.getInteger(KEY_SAMPLE_RATE)
+            val channelCount = format.getInteger(KEY_CHANNEL_COUNT)
+            val buffer = pool.take(head.buffer)
+            head.release()
             queue.addFirst(head.copy(
-                    timeUs = shortsToUs(consumed, sampleRate, channels)
+                timeUs = shortsToUs(consumed, sampleRate, channelCount),
+                release = { pool.give(buffer) },
+                buffer = buffer
             ))
+            log.v("drain(): partially handled chunk at ${head.timeUs}us, ${head.buffer.remaining()} bytes left (${queue.size})")
         } else {
             // buffer consumed!
+            log.v("drain(): consumed chunk at ${head.timeUs}us (${queue.size + 1} => ${queue.size})")
             head.release()
         }
         return result
     }
 }
+
+
+class ShortBufferPool {
+    private val pool = mutableListOf<ShortBuffer>()
+
+    fun take(original: ShortBuffer): ShortBuffer {
+        val needed = original.remaining()
+        val index = pool.indexOfFirst { it.capacity() >= needed }
+        val memory = when {
+            index >= 0 -> pool.removeAt(index)
+            else -> ByteBuffer.allocateDirect((needed * Short.SIZE_BYTES).coerceAtLeast(1024))
+                .order(original.order())
+                .asShortBuffer()
+        }
+        memory.put(original)
+        memory.flip()
+        return memory
+    }
+
+    fun give(buffer: ShortBuffer) {
+        buffer.clear()
+        pool.add(buffer)
+    }
+}
\ 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 fac8b7cd..1810d0a6 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
@@ -23,11 +23,11 @@ internal interface AudioRemixer {
 
     companion object {
         internal operator fun get(inputChannels: Int, outputChannels: Int): AudioRemixer = when {
+            inputChannels == outputChannels -> PassThroughAudioRemixer()
             inputChannels !in setOf(1, 2) -> error("Input channel count not supported: $inputChannels")
-            outputChannels !in setOf(1, 2) -> error("Input channel count not supported: $inputChannels")
+            outputChannels !in setOf(1, 2) -> error("Output channel count not supported: $outputChannels")
             inputChannels < outputChannels -> UpMixAudioRemixer()
-            inputChannels > outputChannels -> DownMixAudioRemixer()
-            else -> PassThroughAudioRemixer()
+            else -> DownMixAudioRemixer()
         }
     }
 }
\ 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 fba72dd9..635536a6 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
@@ -3,26 +3,21 @@ package com.otaliastudios.transcoder.internal.codec
 import android.media.MediaCodec.*
 import android.media.MediaFormat
 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.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
-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
 
 
 internal 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
 )
 
 internal interface DecoderChannel : Channel {
@@ -31,93 +26,89 @@ internal interface DecoderChannel : Channel {
 }
 
 internal 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 {
-
-    companion object {
-        private val ID = trackMapOf(AtomicInteger(0), AtomicInteger(0))
+    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>(
+    when (format.trackType) {
+        TrackType.VIDEO -> "VideoDecoder"
+        TrackType.AUDIO -> "AudioDecoder"
     }
+), ReaderChannel {
 
-    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 buffers by lazy { MediaCodecBuffers(codec) }
+    init {
+        log.i("init: instantiating codec...")
+    }
+    private val decoder = Codecs.Codec(createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!), null, log)
     private var info = BufferInfo()
     private val dropper = DecoderDropper(continuous)
 
-    private var dequeuedInputs by observable(0) { _, _, _ -> printDequeued() }
-    private var dequeuedOutputs by observable(0) { _, _, _ -> printDequeued() }
-    private fun printDequeued() {
-        // log.v("dequeuedInputs=$dequeuedInputs dequeuedOutputs=$dequeuedOutputs")
-    }
+    private var surfaceRendering = false
+    private val surfaceRenderingDummyBuffer = ByteBuffer.allocateDirect(0)
 
     override fun initialize(next: DecoderChannel) {
         super.initialize(next)
         log.i("initialize()")
         val surface = next.handleSourceFormat(format)
-        codec.configure(format, surface, null, 0)
-        codec.start()
+        surfaceRendering = surface != null
+        decoder.codec.configure(format, surface, null, 0)
+        decoder.codec.start()
     }
 
-    override fun buffer(): Pair<ByteBuffer, Int>? {
-        val id = codec.dequeueInputBuffer(0)
-        return if (id >= 0) {
-            dequeuedInputs++
-            buffers.getInputBuffer(id) to id
-        } else {
-            log.i("buffer() failed. dequeuedInputs=$dequeuedInputs dequeuedOutputs=$dequeuedOutputs")
-            null
-        }
-    }
+    override fun buffer(): Pair<ByteBuffer, Int>? = decoder.getInputBuffer()
 
     override fun enqueueEos(data: ReaderData) {
         log.i("enqueueEos()!")
-        dequeuedInputs--
-        val flag = BUFFER_FLAG_END_OF_STREAM
-        codec.queueInputBuffer(data.id, 0, 0, 0, flag)
+        decoder.dequeuedInputs--
+        decoder.codec.queueInputBuffer(data.id, 0, 0, 0, BUFFER_FLAG_END_OF_STREAM)
     }
 
     override fun enqueue(data: ReaderData) {
-        dequeuedInputs--
+        decoder.dequeuedInputs--
         val (chunk, id) = data
         val flag = if (chunk.keyframe) BUFFER_FLAG_SYNC_FRAME else 0
-        codec.queueInputBuffer(id, chunk.buffer.position(), chunk.buffer.remaining(), chunk.timeUs, flag)
+        log.v("enqueued ${chunk.buffer.remaining()} bytes (${chunk.timeUs}us)")
+        decoder.codec.queueInputBuffer(id, chunk.buffer.position(), chunk.buffer.remaining(), chunk.timeUs, flag)
         dropper.input(chunk.timeUs, chunk.render)
     }
 
     override fun drain(): State<DecoderData> {
-        val result = codec.dequeueOutputBuffer(info, 0)
+        val result = decoder.codec.dequeueOutputBuffer(info, 100)
         return when (result) {
             INFO_TRY_AGAIN_LATER -> {
                 log.i("drain(): got INFO_TRY_AGAIN_LATER, waiting.")
-                State.Wait
+                State.Retry(true)
             }
             INFO_OUTPUT_FORMAT_CHANGED -> {
-                log.i("drain(): got INFO_OUTPUT_FORMAT_CHANGED, handling format and retrying. format=${codec.outputFormat}")
-                next.handleRawFormat(codec.outputFormat)
-                State.Retry
+                log.i("drain(): got INFO_OUTPUT_FORMAT_CHANGED, handling format and retrying. format=${decoder.codec.outputFormat}")
+                next.handleRawFormat(decoder.codec.outputFormat)
+                drain()
             }
             INFO_OUTPUT_BUFFERS_CHANGED -> {
                 log.i("drain(): got INFO_OUTPUT_BUFFERS_CHANGED, retrying.")
-                buffers.onOutputBuffersChanged()
-                State.Retry
+                drain()
             }
             else -> {
                 val isEos = info.flags and BUFFER_FLAG_END_OF_STREAM != 0
                 val timeUs = if (isEos) 0 else dropper.output(info.presentationTimeUs)
                 if (timeUs != null /* && (isEos || info.size > 0) */) {
-                    dequeuedOutputs++
-                    val buffer = buffers.getOutputBuffer(result)
+                    val codecBuffer = decoder.codec.getOutputBuffer(result)
+                    val buffer = when {
+                        codecBuffer != null -> codecBuffer
+                        surfaceRendering -> surfaceRenderingDummyBuffer // happens, at least on API28 emulator
+                        else -> error("outputBuffer($result, ${info.size}, ${info.offset}, ${info.flags}) should not be null.")
+                    }
+                    decoder.dequeuedOutputs++
                     val data = DecoderData(buffer, timeUs) {
-                        codec.releaseOutputBuffer(result, it)
-                        dequeuedOutputs--
+                        decoder.codec.releaseOutputBuffer(result, it)
+                        decoder.dequeuedOutputs--
                     }
                     if (isEos) State.Eos(data) else State.Ok(data)
                 } else {
-                    codec.releaseOutputBuffer(result, false)
-                    State.Wait
+                    // frame was dropped, no need to sleep
+                    decoder.codec.releaseOutputBuffer(result, false)
+                    State.Retry(false)
                 }.also {
                     log.v("drain(): returning $it")
                 }
@@ -126,8 +117,8 @@ internal class Decoder(
     }
 
     override fun release() {
-        log.i("release(): releasing codec. dequeuedInputs=$dequeuedInputs dequeuedOutputs=$dequeuedOutputs")
-        codec.stop()
-        codec.release()
+        log.i("release: releasing codec. ${decoder.state}")
+        decoder.codec.stop()
+        decoder.codec.release()
     }
-}
\ 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 4c9459a6..6b5adf9c 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
@@ -1,7 +1,7 @@
 package com.otaliastudios.transcoder.internal.codec
 
 import com.otaliastudios.transcoder.common.TrackType
-import com.otaliastudios.transcoder.internal.pipeline.DataStep
+import com.otaliastudios.transcoder.internal.pipeline.TransformStep
 import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.time.TimeInterpolator
 import java.nio.ByteBuffer
@@ -15,38 +15,38 @@ internal class DecoderTimerData(
 ) : DecoderData(buffer, timeUs, release)
 
 internal class DecoderTimer(
-        private val track: TrackType,
-        private val interpolator: TimeInterpolator,
-) : DataStep<DecoderData, DecoderChannel>() {
+    private val track: TrackType,
+    private val interpolator: TimeInterpolator,
+) : TransformStep<DecoderData, DecoderChannel>("DecoderTimer") {
 
-    private var lastTimeUs: Long? = null
-    private var lastRawTimeUs: Long? = null
+    private var lastTimeUs: Long = Long.MIN_VALUE
+    private var lastRawTimeUs: Long = Long.MIN_VALUE
 
-    override fun step(state: State.Ok<DecoderData>, fresh: Boolean): State<DecoderData> {
+    override fun advance(state: State.Ok<DecoderData>): State<DecoderData> {
         if (state is State.Eos) return state
         require(state.value !is DecoderTimerData) {
             "Can't apply DecoderTimer twice."
         }
         val rawTimeUs = state.value.timeUs
         val timeUs = interpolator.interpolate(track, rawTimeUs)
-        val timeStretch = if (lastTimeUs == null) {
+        val timeStretch = if (lastTimeUs == Long.MIN_VALUE) {
             1.0
         } else {
             // TODO to be exact, timeStretch should be computed by comparing the NEXT timestamps
             //  with this, instead of comparing this with the PREVIOUS
-            val durationUs = timeUs - lastTimeUs!!
-            val rawDurationUs = rawTimeUs - lastRawTimeUs!!
+            val durationUs = timeUs - lastTimeUs
+            val rawDurationUs = rawTimeUs - lastRawTimeUs
             durationUs.toDouble() / rawDurationUs
         }
         lastTimeUs = timeUs
         lastRawTimeUs = rawTimeUs
 
         return State.Ok(DecoderTimerData(
-                buffer = state.value.buffer,
-                rawTimeUs = rawTimeUs,
-                timeUs = timeUs,
-                timeStretch = timeStretch,
-                release = state.value.release
+            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 730c5633..38fcfdfc 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,23 +1,14 @@
 package com.otaliastudios.transcoder.internal.codec
 
-import android.media.MediaCodec
 import android.media.MediaCodec.*
-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
-import com.otaliastudios.transcoder.internal.media.MediaCodecBuffers
 import com.otaliastudios.transcoder.internal.pipeline.Channel
 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 java.nio.ByteBuffer
-import java.util.concurrent.atomic.AtomicInteger
-import kotlin.properties.Delegates
-import kotlin.properties.Delegates.observable
 
 internal data class EncoderData(
         val buffer: ByteBuffer?, // If present, it must have correct position/remaining!
@@ -28,72 +19,56 @@ internal data class EncoderData(
 }
 
 internal interface EncoderChannel : Channel {
-    val surface: Surface?
+    val surface: Codecs.Surface?
     fun buffer(): Pair<ByteBuffer, Int>?
 }
 
 internal class Encoder(
-        private val codec: MediaCodec,
-        override val surface: Surface?,
-        ownsCodecStart: Boolean,
-        private val ownsCodecStop: Boolean,
-) : QueuedStep<EncoderData, EncoderChannel, WriterData, WriterChannel>(), EncoderChannel {
+    private val encoder: Codecs.Codec,
+    ownsCodecStart: Boolean,
+    private val ownsCodecStop: Boolean,
+) : QueuedStep<EncoderData, EncoderChannel, WriterData, WriterChannel>(
+    when (encoder.surface) {
+        null -> "AudioEncoder"
+        else -> "VideoEncoder"
+    }
+), EncoderChannel {
 
     constructor(codecs: Codecs, type: TrackType) : this(
-            codecs.encoders[type].first,
-            codecs.encoders[type].second,
-            codecs.ownsEncoderStart[type],
-            codecs.ownsEncoderStop[type]
+        codecs.encoders[type],
+        codecs.ownsEncoderStart[type],
+        codecs.ownsEncoderStop[type]
     )
 
-    companion object {
-        private val ID = trackMapOf(AtomicInteger(0), AtomicInteger(0))
-    }
-
-    private val type = if (surface != null) TrackType.VIDEO else TrackType.AUDIO
-    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() {
-        log.v("dequeuedInputs=$dequeuedInputs dequeuedOutputs=$dequeuedOutputs")
-    }
-
+    override val surface: Codecs.Surface? get() = encoder.surface
     override val channel = this
 
-    private val buffers by lazy { MediaCodecBuffers(codec) }
-
     private var info = BufferInfo()
 
-
     init {
-        log.i("Encoder: ownsStart=$ownsCodecStart ownsStop=$ownsCodecStop")
+        encoder.log = log
+        log.i("ownsStart=$ownsCodecStart ownsStop=$ownsCodecStop ${encoder.state}")
         if (ownsCodecStart) {
-            codec.start()
+            encoder.codec.start()
         }
     }
 
-    override fun buffer(): Pair<ByteBuffer, Int>? {
-        val id = codec.dequeueInputBuffer(0)
-        return if (id >= 0) {
-            dequeuedInputs++
-            buffers.getInputBuffer(id) to id
-        } else {
-            log.i("buffer() failed. dequeuedInputs=$dequeuedInputs dequeuedOutputs=$dequeuedOutputs")
-            null
-        }
-    }
+    override fun buffer(): Pair<ByteBuffer, Int>? = encoder.getInputBuffer()
 
     private var eosReceivedButNotEnqueued = false
 
     override fun enqueueEos(data: EncoderData) {
         if (surface == null) {
-            if (!ownsCodecStop) eosReceivedButNotEnqueued = true
-            val flag = if (!ownsCodecStop) 0 else BUFFER_FLAG_END_OF_STREAM
-            codec.queueInputBuffer(data.id, 0, 0, 0, flag)
-            dequeuedInputs--
+            if (ownsCodecStop) {
+                encoder.codec.queueInputBuffer(data.id, 0, 0, 0, BUFFER_FLAG_END_OF_STREAM)
+                encoder.dequeuedInputs--
+            } else {
+                eosReceivedButNotEnqueued = true
+                encoder.holdInputBuffer(data.buffer!!, data.id)
+            }
         } else {
             if (!ownsCodecStop) eosReceivedButNotEnqueued = true
-            else codec.signalEndOfInputStream()
+            else encoder.codec.signalEndOfInputStream()
         }
     }
 
@@ -101,52 +76,51 @@ internal class Encoder(
         if (surface != null) return
         else {
             val buffer = requireNotNull(data.buffer) { "Audio should always pass a buffer to Encoder." }
-            codec.queueInputBuffer(data.id, buffer.position(), buffer.remaining(), data.timeUs, 0)
-            dequeuedInputs--
+            encoder.codec.queueInputBuffer(data.id, buffer.position(), buffer.remaining(), data.timeUs, 0)
+            encoder.dequeuedInputs--
         }
     }
 
     override fun drain(): State<WriterData> {
-        val timeoutUs = if (eosReceivedButNotEnqueued) 5000L else 0L
-        return when (val result = codec.dequeueOutputBuffer(info, timeoutUs)) {
+        val timeoutUs = if (eosReceivedButNotEnqueued) 5000L else 100L
+        return when (val result = encoder.codec.dequeueOutputBuffer(info, timeoutUs)) {
             INFO_TRY_AGAIN_LATER -> {
                 if (eosReceivedButNotEnqueued) {
                     // Horrible hack. When we don't own the MediaCodec, we can't enqueue EOS so we
                     // can't dequeue them. INFO_TRY_AGAIN_LATER is returned. We assume this means EOS.
-                    log.i("Sending fake Eos. dequeuedInputs=$dequeuedInputs dequeuedOutputs=$dequeuedOutputs")
+                    log.i("Sending fake Eos. ${encoder.state}")
                     val buffer = ByteBuffer.allocateDirect(0)
                     State.Eos(WriterData(buffer, 0L, 0) {})
                 } else {
                     log.i("Can't dequeue output buffer: INFO_TRY_AGAIN_LATER")
-                    State.Wait
+                    State.Retry(true)
                 }
             }
             INFO_OUTPUT_FORMAT_CHANGED -> {
-                log.i("INFO_OUTPUT_FORMAT_CHANGED! format=${codec.outputFormat}")
-                next.handleFormat(codec.outputFormat)
-                State.Retry
+                log.i("INFO_OUTPUT_FORMAT_CHANGED! format=${encoder.codec.outputFormat}")
+                next.handleFormat(encoder.codec.outputFormat)
+                drain()
             }
             INFO_OUTPUT_BUFFERS_CHANGED -> {
-                buffers.onOutputBuffersChanged()
-                State.Retry
+                drain()
             }
             else -> {
                 val isConfig = info.flags and BUFFER_FLAG_CODEC_CONFIG != 0
                 if (isConfig) {
-                    codec.releaseOutputBuffer(result, false)
-                    State.Retry
+                    encoder.codec.releaseOutputBuffer(result, false)
+                    drain()
                 } else {
-                    dequeuedOutputs++
+                    encoder.dequeuedOutputs++
                     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 buffer = checkNotNull(encoder.codec.getOutputBuffer(result)) { "outputBuffer($result) should not be null." }
                     val timeUs = info.presentationTimeUs
                     buffer.clear()
                     buffer.limit(info.offset + info.size)
                     buffer.position(info.offset)
                     val data = WriterData(buffer, timeUs, flags) {
-                        codec.releaseOutputBuffer(result, false)
-                        dequeuedOutputs--
+                        encoder.codec.releaseOutputBuffer(result, false)
+                        encoder.dequeuedOutputs--
                     }
                     if (isEos) State.Eos(data) else State.Ok(data)
                 }
@@ -155,9 +129,9 @@ internal class Encoder(
     }
 
     override fun release() {
-        log.i("release(): ownsStop=$ownsCodecStop dequeuedInputs=${dequeuedInputs} dequeuedOutputs=$dequeuedOutputs")
+        log.i("release(): ownsStop=$ownsCodecStop ${encoder.state}")
         if (ownsCodecStop) {
-            codec.stop()
+            encoder.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 5a75f657..8bd3529b 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
@@ -2,6 +2,7 @@ package com.otaliastudios.transcoder.internal.data
 
 import android.media.MediaCodec
 import android.media.MediaFormat
+import com.otaliastudios.transcoder.internal.pipeline.BaseStep
 import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.internal.pipeline.Step
 import com.otaliastudios.transcoder.internal.utils.Logger
@@ -9,9 +10,8 @@ import java.nio.ByteBuffer
 import java.nio.ByteOrder
 
 internal class Bridge(private val format: MediaFormat)
-    : Step<ReaderData, ReaderChannel, WriterData, WriterChannel>, ReaderChannel {
+    : BaseStep<ReaderData, ReaderChannel, WriterData, WriterChannel>("Bridge"), ReaderChannel {
 
-    private val log = Logger("Bridge")
     private val bufferSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
     private val buffer = ByteBuffer.allocateDirect(bufferSize).order(ByteOrder.nativeOrder())
     override val channel = this
@@ -27,7 +27,7 @@ internal class Bridge(private val format: MediaFormat)
     }
 
     // Can't do much about chunk.render, since we don't even decode.
-    override fun step(state: State.Ok<ReaderData>, fresh: Boolean): State<WriterData> {
+    override fun advance(state: State.Ok<ReaderData>): State<WriterData> {
         val (chunk, _) = state.value
         val flags = if (chunk.keyframe) MediaCodec.BUFFER_FLAG_SYNC_FRAME else 0
         val result = WriterData(chunk.buffer, chunk.timeUs, flags) {}
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..c663f6b7 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
@@ -4,7 +4,6 @@ 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
 
@@ -16,25 +15,25 @@ internal interface ReaderChannel : Channel {
 }
 
 internal class Reader(
-        private val source: DataSource,
-        private val track: TrackType
-) : BaseStep<Unit, Channel, ReaderData, ReaderChannel>() {
+    private val source: DataSource,
+    private val track: TrackType
+) : BaseStep<Unit, Channel, ReaderData, ReaderChannel>("Reader") {
 
-    private val log = Logger("Reader")
     override val channel = Channel
     private val chunk = DataSource.Chunk()
 
     private inline fun nextBufferOrWait(action: (ByteBuffer, Int) -> State<ReaderData>): State<ReaderData> {
         val buffer = next.buffer()
         if (buffer == null) {
+            // dequeueInputBuffer failed
             log.v("Returning State.Wait because buffer is null.")
-            return State.Wait
+            return State.Retry(true)
         } else {
             return action(buffer.first, buffer.second)
         }
     }
 
-    override fun step(state: State.Ok<Unit>, fresh: Boolean): State<ReaderData> {
+    override fun advance(state: State.Ok<Unit>): State<ReaderData> {
         return if (source.isDrained) {
             log.i("Source is drained! Returning Eos as soon as possible.")
             nextBufferOrWait { byteBuffer, id ->
@@ -46,13 +45,14 @@ internal class Reader(
             }
         } else if (!source.canReadTrack(track)) {
             log.i("Returning State.Wait because source can't read $track right now.")
-            State.Wait
+            State.Retry(false)
         } else {
             nextBufferOrWait { byteBuffer, id ->
                 chunk.buffer = byteBuffer
                 source.readTrack(chunk)
+                // log.v("Returning ${chunk.buffer?.remaining() ?: -1} bytes from source")
                 State.Ok(ReaderData(chunk, id))
             }
         }
     }
-}
\ 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 57a48562..b0996afe 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
@@ -1,15 +1,15 @@
 package com.otaliastudios.transcoder.internal.data
 
 import com.otaliastudios.transcoder.common.TrackType
-import com.otaliastudios.transcoder.internal.pipeline.DataStep
+import com.otaliastudios.transcoder.internal.pipeline.TransformStep
 import com.otaliastudios.transcoder.internal.pipeline.State
 import com.otaliastudios.transcoder.time.TimeInterpolator
 
 internal class ReaderTimer(
-        private val track: TrackType,
-        private val interpolator: TimeInterpolator
-) : DataStep<ReaderData, ReaderChannel>() {
-    override fun step(state: State.Ok<ReaderData>, fresh: Boolean): State<ReaderData> {
+    private val track: TrackType,
+    private val interpolator: TimeInterpolator
+) : TransformStep<ReaderData, ReaderChannel>("ReaderTimer") {
+    override fun advance(state: State.Ok<ReaderData>): State<ReaderData> {
         if (state is State.Eos) return state
         state.value.chunk.timeUs = interpolator.interpolate(track, state.value.chunk.timeUs)
         return state
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..4e2ade8d 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
@@ -9,16 +9,15 @@ import com.otaliastudios.transcoder.source.DataSource
 import java.nio.ByteBuffer
 
 internal class Seeker(
-        private val source: DataSource,
-        positions: List<Long>,
-        private val seek: (Long) -> Boolean
-) : BaseStep<Unit, Channel, Unit, Channel>() {
+    private val source: DataSource,
+    positions: List<Long>,
+    private val seek: (Long) -> Boolean
+) : BaseStep<Unit, Channel, Unit, Channel>("Seeker") {
 
-    private val log = Logger("Seeker")
     override val channel = Channel
     private val positions = positions.toMutableList()
 
-    override fun step(state: State.Ok<Unit>, fresh: Boolean): State<Unit> {
+    override fun advance(state: State.Ok<Unit>): State<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/data/Writer.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt
index 1d8fc899..323edbf0 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
@@ -3,6 +3,7 @@ package com.otaliastudios.transcoder.internal.data
 import android.media.MediaCodec
 import android.media.MediaFormat
 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.pipeline.Step
@@ -22,13 +23,12 @@ internal interface WriterChannel : Channel {
 }
 
 internal class Writer(
-        private val sink: DataSink,
-        private val track: TrackType
-) : Step<WriterData, WriterChannel, Unit, Channel>, WriterChannel {
+    private val sink: DataSink,
+    private val track: TrackType
+) : BaseStep<WriterData, WriterChannel, Unit, Channel>("Writer"), WriterChannel {
 
     override val channel = this
 
-    private val log = Logger("Writer")
     private val info = MediaCodec.BufferInfo()
 
     override fun handleFormat(format: MediaFormat) {
@@ -36,17 +36,24 @@ internal class Writer(
         sink.setTrackFormat(track, format)
     }
 
-    override fun step(state: State.Ok<WriterData>, fresh: Boolean): State<Unit> {
+    override fun advance(state: State.Ok<WriterData>): State<Unit> {
         val (buffer, timestamp, flags) = state.value
+        // Note: flags does NOT include BUFFER_FLAG_END_OF_STREAM. That's passed via State.Eos.
         val eos = state is State.Eos
-        info.set(
+        if (eos) {
+            // Note: it may happen that at this point, buffer has some data. but creating an extra writeTrack() call
+            // can cause some crashes that were not properly debugged, probably related to wrong timestamp.
+            // I think if we could ensure that timestamp is valid (> previous, > 0) and buffer.hasRemaining(), there should
+            // be an extra call here. See #159. Reluctant to do so without a repro test.
+            info.set(0, 0, 0, flags or MediaCodec.BUFFER_FLAG_END_OF_STREAM)
+        } else {
+            info.set(
                 buffer.position(),
                 buffer.remaining(),
                 timestamp,
-                if (eos) {
-                    flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM
-                } else flags
-        )
+                flags
+            )
+        }
         sink.writeTrack(track, buffer, info)
         state.value.release()
         return if (eos) State.Eos(Unit) else State.Ok(Unit)
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/media/MediaCodecBuffers.java b/lib/src/main/java/com/otaliastudios/transcoder/internal/media/MediaCodecBuffers.java
deleted file mode 100644
index fe09ff32..00000000
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/media/MediaCodecBuffers.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package com.otaliastudios.transcoder.internal.media;
-
-import android.media.MediaCodec;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-
-import java.nio.ByteBuffer;
-
-/**
- * A Wrapper to MediaCodec that facilitates the use of API-dependent get{Input/Output}Buffer methods,
- * in order to prevent: http://stackoverflow.com/q/30646885
- */
-public class MediaCodecBuffers {
-
-    private final MediaCodec mMediaCodec;
-    private final ByteBuffer[] mInputBuffers;
-    private ByteBuffer[] mOutputBuffers;
-
-    public MediaCodecBuffers(@NonNull MediaCodec mediaCodec) {
-        mMediaCodec = mediaCodec;
-
-        if (Build.VERSION.SDK_INT < 21) {
-            mInputBuffers = mediaCodec.getInputBuffers();
-            mOutputBuffers = mediaCodec.getOutputBuffers();
-        } else {
-            mInputBuffers = mOutputBuffers = null;
-        }
-    }
-
-    @NonNull
-    public ByteBuffer getInputBuffer(final int index) {
-        if (Build.VERSION.SDK_INT >= 21) {
-            // This is nullable only for incorrect usage.
-            return mMediaCodec.getInputBuffer(index);
-        }
-        ByteBuffer result = mInputBuffers[index];
-        result.clear();
-        return result;
-    }
-
-    @NonNull
-    public ByteBuffer getOutputBuffer(final int index) {
-        if (Build.VERSION.SDK_INT >= 21) {
-            // This is nullable only for incorrect usage.
-            return mMediaCodec.getOutputBuffer(index);
-        }
-        return mOutputBuffers[index];
-    }
-
-    public void onOutputBuffersChanged() {
-        if (Build.VERSION.SDK_INT < 21) {
-            mOutputBuffers = mMediaCodec.getOutputBuffers();
-        }
-    }
-}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/media/MediaFormatProvider.java b/lib/src/main/java/com/otaliastudios/transcoder/internal/media/MediaFormatProvider.java
index fd0c5f31..d642ec81 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/media/MediaFormatProvider.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/media/MediaFormatProvider.java
@@ -99,12 +99,11 @@ private MediaFormat decodeMediaFormat(@NonNull DataSource source,
             throw new RuntimeException("Can't decode this track", e);
         }
         decoder.start();
-        MediaCodecBuffers buffers = new MediaCodecBuffers(decoder);
         MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
         DataSource.Chunk chunk = new DataSource.Chunk();
         MediaFormat result = null;
         while (result == null) {
-            result = decodeOnce(type, source, chunk, decoder, buffers, info);
+            result = decodeOnce(type, source, chunk, decoder, info);
         }
         source.deinitialize();
         source.initialize();
@@ -116,18 +115,16 @@ private MediaFormat decodeOnce(@NonNull TrackType type,
                                    @NonNull DataSource source,
                                    @NonNull DataSource.Chunk chunk,
                                    @NonNull MediaCodec decoder,
-                                   @NonNull MediaCodecBuffers buffers,
                                    @NonNull MediaCodec.BufferInfo info) {
         // First drain then feed.
-        MediaFormat format = drainOnce(decoder, buffers, info);
+        MediaFormat format = drainOnce(decoder, info);
         if (format != null) return format;
-        feedOnce(type, source, chunk, decoder, buffers);
+        feedOnce(type, source, chunk, decoder);
         return null;
     }
 
     @Nullable
     private MediaFormat drainOnce(@NonNull MediaCodec decoder,
-                                  @NonNull MediaCodecBuffers buffers,
                                   @NonNull MediaCodec.BufferInfo info) {
         int result = decoder.dequeueOutputBuffer(info, 0);
         switch (result) {
@@ -136,8 +133,7 @@ private MediaFormat drainOnce(@NonNull MediaCodec decoder,
             case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                 return decoder.getOutputFormat();
             case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
-                buffers.onOutputBuffersChanged();
-                return drainOnce(decoder, buffers, info);
+                return drainOnce(decoder, info);
             default: // Drop this data immediately.
                 decoder.releaseOutputBuffer(result, false);
                 return null;
@@ -147,14 +143,13 @@ private MediaFormat drainOnce(@NonNull MediaCodec decoder,
     private void feedOnce(@NonNull TrackType type,
                           @NonNull DataSource source,
                           @NonNull DataSource.Chunk chunk,
-                          @NonNull MediaCodec decoder,
-                          @NonNull MediaCodecBuffers buffers) {
+                          @NonNull MediaCodec decoder) {
         if (!source.canReadTrack(type)) {
             throw new RuntimeException("This should never happen!");
         }
         final int result = decoder.dequeueInputBuffer(0);
         if (result < 0) return;
-        chunk.buffer = buffers.getInputBuffer(result);
+        chunk.buffer = decoder.getInputBuffer(result);
         source.readTrack(chunk);
         decoder.queueInputBuffer(result,
                 chunk.buffer.position(),
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..85fd5ab0 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,63 +3,178 @@ package com.otaliastudios.transcoder.internal.pipeline
 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>) {
+private class PipelineItem(
+    val step: Step<Any, Channel, Any, Channel>,
+    val name: String,
+) {
+    // var success: State.Ok<Any>? = null
+    // var failure: State.Retry? = null
+    val unhandled = ArrayDeque<State.Ok<Any>>()
+    var done = false
+    var advanced = false
+    var packets = 0
+    private var nextUnhandled: ArrayDeque<State.Ok<Any>>? = null
 
-    private val log = Logger("Pipeline($name)")
-    private var headState: State.Ok<Any> = State.Ok(Unit)
-    private var headIndex = 0
+    fun attachToNext(next: PipelineItem) {
+        nextUnhandled = next.unhandled
+        step.initialize(next = next.step.channel)
+    }
 
-    init {
-        chain.zipWithNext().reversed().forEach { (first, next) ->
-            first.initialize(next = next.channel)
+    fun canHandle(first: Boolean): Boolean {
+        if (done) return false
+        if (first) {
+            unhandled.clear()
+            unhandled.addLast(State.Ok(Unit))
         }
+        return unhandled.isNotEmpty() || step is QueuedStep
     }
 
-    // Returns Eos, Ok or Wait
-    fun execute(): State<Unit> {
-        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
+    fun handle(): State.Failure? {
+        advanced = false
+        while (unhandled.isNotEmpty() && !done) {
+            val input = unhandled.removeFirst()
+            when (val result = step.advance(input)) {
+                is State.Ok -> {
+                    packets++
+                    advanced = true
+                    done = result is State.Eos
+                    nextUnhandled?.addLast(result)
+                }
+                is State.Retry -> {
+                    unhandled.addFirst(input)
+                    return result
+                }
+                is State.Consume -> return result
             }
-            // 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
+        }
+        if (!advanced && !done && step is QueuedStep) {
+            when (val result = step.tryAdvance()) {
+                is State.Ok -> {
+                    packets++
+                    advanced = true
+                    done = result is State.Eos
+                    nextUnhandled?.addLast(result)
+                }
+                is State.Failure -> return result
+            }
+        }
+        return null
+    }
+}
+
+internal class Pipeline private constructor(name: String, private val items: List<PipelineItem>) {
+
+    private val log = Logger(name)
+
+    init {
+        items.zipWithNext().reversed().forEach { (first, next) -> first.attachToNext(next) }
+    }
+
+    fun execute(): State<Unit> {
+        log.v("LOOP")
+        var advanced = false
+        var sleeps = false
+
+        for (i in items.indices) {
+            val item = items[i]
+
+            if (item.canHandle(i == 0)) {
+                log.v("${item.name} START #${item.packets} (${item.unhandled.size} pending)")
+                val failure = item.handle()
+                if (failure != null) {
+                    sleeps = sleeps || failure.sleep
+                    log.v("${item.name} FAILED #${item.packets}")
+                } else {
+                    log.v("${item.name} SUCCESS #${item.packets} ${if (item.done) "(eos)" else ""}")
+                }
+                advanced = advanced || item.advanced
+            } else {
+                log.v("${item.name} SKIP #${item.packets} ${if (item.done) "(eos)" else ""}")
             }
         }
         return when {
-            chain.isEmpty() -> State.Eos(Unit)
-            state is State.Eos -> State.Eos(Unit)
-            else -> State.Ok(Unit)
+            items.isEmpty() -> State.Eos(Unit)
+            items.last().done -> State.Eos(Unit)
+            advanced -> State.Ok(Unit)
+            else -> State.Retry(sleeps)
         }
     }
 
-    fun release() {
-        chain.forEach { it.release() }
-    }
+    /*fun execute_OLD(): State<Unit> {
+        var headState: State.Ok<Any> = State.Ok(Unit)
+        var headFresh = true
+        // In case of failure in the previous run, we should re-run all items before the failed one
+        // This is important for decoders/encoders that need more input before they can output
+        val previouslyFailedIndex = items.indexOfLast { it.failure != null }.takeIf { it >= 0 }
+        log.v("LOOP (previouslyFailed: ${previouslyFailedIndex})")
+        for (i in items.indices) {
+            val item = items[i]
+            val cached = item.success
+            val skip = cached is State.Eos || (!headFresh && cached != null)
+            if (skip) {
+                // Note: we could consider a retry() on queued steps here but it's risky
+                // because the current 'cached' value may have never been handled by the next item
+                log.v("${i+1}/${items.size} '${item.step.name}' SKIP ${if (cached is State.Eos) "(eos)" else "(handled)"}")
+                headState = cached!!
+                headFresh = false
+                continue
+            }
+            // This item did not succeed at the last loop, or we have fresh input data.
+            log.v("${i+1}/${items.size} '${item.step.name}' START (${if (headFresh) "fresh" else "stale"})")
+            val result = when {
+                !headFresh && item.step is QueuedStep -> item.step.retry() // queued steps should never get stale data
+                else -> item.step.advance(headState)
+            }
+            item.set(result)
+            if (result is State.Ok) {
+                log.v("${i+1}/${items.size} '${item.step.name}' SUCCESS ${if (result is State.Eos) "(eos)" else ""}")
+                headState = result
+                headFresh = true
+                if (i == items.lastIndex) items.forEach {
+                    if (it.success !is State.Eos) it.set(null)
+                }
+                continue
+            }
+            // Item failed. Check if we had a later failure in the previous run. In that case
+            // we should retry that too. Note: `cached` should always be not null in this branch
+            // but let's avoid throwing
+            if (previouslyFailedIndex != null && i < previouslyFailedIndex) {
+                if (cached != null) {
+                    log.v("${i+1}/${items.size} '${item.step.name}' FAILED (skip)")
+                    item.set(cached) // keep 'cached' for next run
+                    headState = cached
+                    headFresh = false
+                    continue
+                }
+            }
 
-    private fun executeStep(previous: State.Ok<Any>, step: AnyStep, fresh: Boolean): State.Ok<Any>? {
-        val state = step.step(previous, fresh)
-        return when (state) {
-            is State.Ok -> state
-            is State.Retry -> executeStep(previous, step, fresh = false)
-            is State.Wait -> null
+            // Item failed: don't proceed. Return early.
+            log.v("${i+1}/${items.size} '${item.step.name}' FAILED")
+            return State.Wait(item.failure!!.sleep)
         }
+        return when {
+            items.isEmpty() -> State.Eos(Unit)
+            headState is State.Eos -> State.Eos(Unit)
+            else -> State.Ok(Unit)
+        }
+    } */
+
+    fun release() {
+        items.forEach { it.step.release() }
     }
 
     companion object {
-        @Suppress("UNCHECKED_CAST")
-        internal fun build(name: String, builder: () -> Builder<*, Channel> = { Builder<Unit, Channel>() }): Pipeline {
-            return Pipeline(name, builder().steps as List<AnyStep>)
+        internal fun build(name: String, debug: String? = null, builder: () -> Builder<*, Channel> = { Builder<Unit, Channel>() }): Pipeline {
+            val steps = builder().steps
+            val items = steps.mapIndexed { index, step ->
+                @Suppress("UNCHECKED_CAST")
+                PipelineItem(
+                    step = step as Step<Any, Channel, Any, Channel>,
+                    name = "${index+1}/${steps.size} '${step.name}'"
+                )
+            }
+            return Pipeline("${name}Pipeline${debug ?: ""}", items)
         }
     }
 
@@ -79,4 +194,4 @@ internal operator fun <
         other: Step<CurrData, CurrChannel, NewData, NewChannel>
 ): Pipeline.Builder<NewData, NewChannel> {
     return Pipeline.Builder<CurrData, CurrChannel>(listOf(this)) + other
-}
\ No newline at end of file
+}
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..2ffc6c51 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,9 +1,9 @@
 package com.otaliastudios.transcoder.internal.pipeline
 
-internal sealed class State<out T> {
+internal sealed interface State<out T> {
 
     // Running
-    open class Ok<T>(val value: T) : State<T>() {
+    open class Ok<T>(val value: T) : State<T> {
         override fun toString() = "State.Ok($value)"
     }
 
@@ -12,13 +12,16 @@ internal sealed class State<out T> {
         override fun toString() = "State.Eos($value)"
     }
 
-    // couldn't run, but might in the future
-    object Wait : State<Nothing>() {
-        override fun toString() = "State.Wait"
+    // Failed to produce output, try again later
+    sealed interface Failure : State<Nothing> {
+        val sleep: Boolean
     }
 
-    // call again as soon as possible
-    object Retry : State<Nothing>() {
-        override fun toString() = "State.Retry"
+    class Retry(override val sleep: Boolean) : Failure {
+        override fun toString() = "State.Retry($sleep)"
     }
-}
\ No newline at end of file
+
+    class Consume(override val sleep: Boolean = false) : Failure {
+        override fun toString() = "State.Consume($sleep)"
+    }
+}
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..cc234575 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,18 +6,17 @@ internal interface Channel {
 }
 
 internal interface Step<
-        Input: Any,
-        InputChannel: Channel,
-        Output: Any,
-        OutputChannel: Channel
+    Input: Any,
+    InputChannel: Channel,
+    Output: Any,
+    OutputChannel: Channel
 > {
+    val name: String
     val channel: InputChannel
 
     fun initialize(next: OutputChannel) = Unit
 
-    fun step(state: State.Ok<Input>, fresh: Boolean): State<Output>
+    fun advance(state: State.Ok<Input>): State<Output>
 
     fun release() = Unit
 }
-
-internal val Step<*, *, *, *>.name get() = this::class.simpleName
\ No newline at end of file
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..6f46892d 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
@@ -22,11 +22,11 @@ import com.otaliastudios.transcoder.time.TimeInterpolator
 internal fun EmptyPipeline() = Pipeline.build("Empty")
 
 internal fun PassThroughPipeline(
-        track: TrackType,
-        source: DataSource,
-        sink: DataSink,
-        interpolator: TimeInterpolator
-) = Pipeline.build("PassThrough($track)") {
+    track: TrackType,
+    source: DataSource,
+    sink: DataSink,
+    interpolator: TimeInterpolator
+) = Pipeline.build("PassThrough$track") {
     Reader(source, track) +
             ReaderTimer(track, interpolator) +
             Bridge(source.getTrackFormat(track)!!) +
@@ -34,28 +34,30 @@ internal fun PassThroughPipeline(
 }
 
 internal fun RegularPipeline(
-        track: TrackType,
-        source: DataSource,
-        sink: DataSink,
-        interpolator: TimeInterpolator,
-        format: MediaFormat,
-        codecs: Codecs,
-        videoRotation: Int,
-        audioStretcher: AudioStretcher,
-        audioResampler: AudioResampler
+    track: TrackType,
+    debug: String?,
+    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(debug, source, sink, interpolator, format, codecs, videoRotation)
+    TrackType.AUDIO -> AudioPipeline(debug, source, sink, interpolator, format, codecs, audioStretcher, audioResampler)
 }
 
 private fun VideoPipeline(
-        source: DataSource,
-        sink: DataSink,
-        interpolator: TimeInterpolator,
-        format: MediaFormat,
-        codecs: Codecs,
-        videoRotation: Int
-) = Pipeline.build("Video") {
+    debug: String?,
+    source: DataSource,
+    sink: DataSink,
+    interpolator: TimeInterpolator,
+    format: MediaFormat,
+    codecs: Codecs,
+    videoRotation: Int
+) = Pipeline.build("Video", debug) {
     Reader(source, TrackType.VIDEO) +
             Decoder(source.getTrackFormat(TrackType.VIDEO)!!, true) +
             DecoderTimer(TrackType.VIDEO, interpolator) +
@@ -66,14 +68,15 @@ private fun VideoPipeline(
 }
 
 private fun AudioPipeline(
-        source: DataSource,
-        sink: DataSink,
-        interpolator: TimeInterpolator,
-        format: MediaFormat,
-        codecs: Codecs,
-        audioStretcher: AudioStretcher,
-        audioResampler: AudioResampler
-) = Pipeline.build("Audio") {
+    debug: String?,
+    source: DataSource,
+    sink: DataSink,
+    interpolator: TimeInterpolator,
+    format: MediaFormat,
+    codecs: Codecs,
+    audioStretcher: AudioStretcher,
+    audioResampler: AudioResampler
+) = Pipeline.build("Audio", debug) {
     Reader(source, TrackType.AUDIO) +
             Decoder(source.getTrackFormat(TrackType.AUDIO)!!, true) +
             DecoderTimer(TrackType.AUDIO, interpolator) +
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..8b39aa7f 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,11 +1,16 @@
 package com.otaliastudios.transcoder.internal.pipeline
 
+import com.otaliastudios.transcoder.internal.utils.Logger
+
 internal abstract class BaseStep<
-        Input: Any,
-        InputChannel: Channel,
-        Output: Any,
-        OutputChannel: Channel
-> : Step<Input, InputChannel, Output, OutputChannel> {
+    Input: Any,
+    InputChannel: Channel,
+    Output: Any,
+    OutputChannel: Channel
+>(final override val name: String) : Step<Input, InputChannel, Output, OutputChannel> {
+
+    protected val log = Logger(name)
+
     protected lateinit var next: OutputChannel
     private set
 
@@ -14,19 +19,20 @@ internal abstract class BaseStep<
     }
 }
 
-internal abstract class DataStep<D: Any, C: Channel> : Step<D, C, D, C> {
+internal abstract class TransformStep<D: Any, C: Channel>(name: String) : BaseStep<D, C, D, C>(name) {
     override lateinit var channel: C
     override fun initialize(next: C) {
+        super.initialize(next)
         channel = next
     }
 }
 
 internal abstract class QueuedStep<
-        Input: Any,
-        InputChannel: Channel,
-        Output: Any,
-        OutputChannel: Channel
-> : BaseStep<Input, InputChannel, Output, OutputChannel>() {
+    Input: Any,
+    InputChannel: Channel,
+    Output: Any,
+    OutputChannel: Channel
+>(name: String) : BaseStep<Input, InputChannel, Output, OutputChannel>(name) {
 
     protected abstract fun enqueue(data: Input)
 
@@ -34,11 +40,18 @@ internal abstract class QueuedStep<
 
     protected abstract fun drain(): State<Output>
 
-    final override fun step(state: State.Ok<Input>, fresh: Boolean): State<Output> {
-        if (fresh) {
-            if (state is State.Eos) enqueueEos(state.value)
-            else enqueue(state.value)
+    final override fun advance(state: State.Ok<Input>): State<Output> {
+        if (state is State.Eos) enqueueEos(state.value)
+        else enqueue(state.value)
+        // Disallow State.Retry because the input was already handled.
+        return when (val result = drain()) {
+            is State.Retry -> State.Consume(result.sleep)
+            else -> result
         }
+    }
+
+    fun tryAdvance(): State<Output> {
         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 7560fa37..13a07bb6 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
@@ -78,6 +78,7 @@ internal class DefaultThumbnailsEngine(
     private fun createPipeline(
             type: TrackType,
             index: Int,
+            count: Int,
             status: TrackStatus,
             outputFormat: MediaFormat
     ): Pipeline {
@@ -134,7 +135,7 @@ internal class DefaultThumbnailsEngine(
     }
 
     companion object {
-        private val WAIT_MS = 10L
+        private val WAIT_MS = 2L
         private val PROGRESS_LOOPS = 10L
     }
 }
\ 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 985d7ff4..260894e2 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
@@ -61,10 +61,11 @@ internal class DefaultTranscodeEngine(
     }
 
     private fun createPipeline(
-            type: TrackType,
-            index: Int,
-            status: TrackStatus,
-            outputFormat: MediaFormat
+        type: TrackType,
+        index: Int,
+        count: Int,
+        status: TrackStatus,
+        outputFormat: MediaFormat
     ): Pipeline {
         log.w("createPipeline($type, $index, $status), format=$outputFormat")
         val interpolator = timer.interpolator(type, index)
@@ -79,7 +80,7 @@ internal class DefaultTranscodeEngine(
             TrackStatus.ABSENT -> EmptyPipeline()
             TrackStatus.REMOVING -> EmptyPipeline()
             TrackStatus.PASS_THROUGH -> PassThroughPipeline(type, source, sink, interpolator)
-            TrackStatus.COMPRESSING -> RegularPipeline(type,
+            TrackStatus.COMPRESSING -> RegularPipeline(type, if (count > 1) "${index+1}/$count" else null,
                     source, sink, interpolator, outputFormat, codecs,
                     videoRotation, audioStretcher, audioResampler)
         }
@@ -114,15 +115,20 @@ internal class DefaultTranscodeEngine(
             val advanced = (audio?.advance() ?: false) or (video?.advance() ?: false)
             val completed = !advanced && !segments.hasNext() // avoid calling hasNext if we advanced.
 
-            log.v("transcode(): executed step=$loop advanced=$advanced completed=$completed")
+            log.v("iteration #$loop audio=${segments.currentIndex.audio+1}/${dataSources.audio.size} video=${segments.currentIndex.video+1}/${dataSources.video.size} advanced=$advanced completed=$completed")
             if (Thread.interrupted()) {
                 throw InterruptedException()
-            } else if (completed) {
+            }
+            if (completed) {
                 progress(1.0)
                 break
-            } else if (!advanced) {
+            }
+
+            if (!advanced && audio?.needsSleep() != false && video?.needsSleep() != false) {
                 Thread.sleep(WAIT_MS)
-            } else if (++loop % PROGRESS_LOOPS == 0L) {
+            }
+
+            if (advanced && ++loop % PROGRESS_LOOPS == 0L) {
                 val audioProgress = timer.progress.audio
                 val videoProgress = timer.progress.video
                 log.v("transcode(): got progress, video=$videoProgress audio=$audioProgress")
@@ -141,7 +147,7 @@ internal class DefaultTranscodeEngine(
 
 
     companion object {
-        private val WAIT_MS = 10L
+        private val WAIT_MS = 2L
         private val PROGRESS_LOOPS = 10L
     }
-}
\ No newline at end of file
+}
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/Logger.java b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/Logger.java
index 50c0b4e5..43c0c291 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/Logger.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/utils/Logger.java
@@ -21,7 +21,7 @@ public class Logger {
     @SuppressWarnings("WeakerAccess")
     public final static int LEVEL_ERROR = 3;
 
-    private static int sLevel;
+    private static int sLevel = LEVEL_INFO;
 
     /**
      * Interface of integers representing log levels.
@@ -36,9 +36,16 @@ public class Logger {
     public @interface LogLevel {}
 
     private final String mTag;
+    private final int mLevel;
 
     public Logger(@NonNull String tag) {
         mTag = tag;
+        mLevel = sLevel;
+    }
+
+    public Logger(@NonNull String tag, int level) {
+        mTag = tag;
+        mLevel = level;
     }
 
     /**
@@ -55,7 +62,7 @@ public static void setLogLevel(@LogLevel int logLevel) {
     }
 
     private boolean should(int messageLevel) {
-        return sLevel <= messageLevel;
+        return mLevel <= messageLevel;
     }
 
     public void v(String message) { v(message, null); }
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..a7773a22 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
@@ -5,36 +5,29 @@ import com.otaliastudios.opengl.core.EglCore
 import com.otaliastudios.opengl.surface.EglWindowSurface
 import com.otaliastudios.transcoder.internal.codec.EncoderChannel
 import com.otaliastudios.transcoder.internal.codec.EncoderData
+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.pipeline.Step
 
 
-internal class VideoPublisher: Step<Long, Channel, EncoderData, EncoderChannel> {
+internal class VideoPublisher: BaseStep<Long, Channel, EncoderData, EncoderChannel>("VideoPublisher") {
 
     override val channel = Channel
 
-    private val core = EglCore(EGL14.EGL_NO_CONTEXT, EglCore.FLAG_RECORDABLE)
-    private lateinit var surface: EglWindowSurface
-
-    override fun initialize(next: EncoderChannel) {
-        super.initialize(next)
-        surface = EglWindowSurface(core, next.surface!!, false)
-        surface.makeCurrent()
-    }
-
-    override fun step(state: State.Ok<Long>, fresh: Boolean): State<EncoderData> {
+    override fun advance(state: State.Ok<Long>): State<EncoderData> {
         if (state is State.Eos) {
             return State.Eos(EncoderData.Empty)
         } else {
-            surface.setPresentationTime(state.value * 1000)
-            surface.swapBuffers()
+            val surface = next.surface!!
+            surface.window.setPresentationTime(state.value * 1000)
+            surface.window.swapBuffers()
+            /* val s = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW)
+            val ss = IntArray(2)
+            EGL14.eglQuerySurface(EGL14.eglGetCurrentDisplay(), s, EGL14.EGL_WIDTH, ss, 0)
+            EGL14.eglQuerySurface(EGL14.eglGetCurrentDisplay(), s, EGL14.EGL_HEIGHT, ss, 1)
+            log.e("XXX VideoPublisher.surfaceSize: ${ss[0]}x${ss[1]}") */
             return State.Ok(EncoderData.Empty)
         }
     }
-
-    override fun release() {
-        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 cf470a66..237eb4f6 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
@@ -6,20 +6,17 @@ import android.view.Surface
 import com.otaliastudios.transcoder.internal.codec.DecoderChannel
 import com.otaliastudios.transcoder.internal.codec.DecoderData
 import com.otaliastudios.transcoder.internal.media.MediaFormatConstants.KEY_ROTATION_DEGREES
+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.pipeline.Step
-import com.otaliastudios.transcoder.internal.utils.Logger
 
 
 internal 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
-): Step<DecoderData, DecoderChannel, Long, Channel>, DecoderChannel {
-
-    private val log = Logger("VideoRenderer")
+    private val sourceRotation: Int, // intrinsic source rotation
+    private val extraRotation: Int, // any extra rotation in TranscoderOptions
+    private val targetFormat: MediaFormat,
+    flipY: Boolean = false
+): BaseStep<DecoderData, DecoderChannel, Long, Channel>("VideoRenderer"), DecoderChannel {
 
     override val channel = this
 
@@ -40,14 +37,17 @@ internal class VideoRenderer(
         val width = targetFormat.getInteger(KEY_WIDTH)
         val height = targetFormat.getInteger(KEY_HEIGHT)
         val flip = extraRotation % 180 != 0
-        log.e("FrameDrawerEncoder: size=$width-$height, flipping=$flip")
-        targetFormat.setInteger(KEY_WIDTH, if (flip) height else width)
-        targetFormat.setInteger(KEY_HEIGHT, if (flip) width else height)
+        val flippedWidth = if (flip) height else width
+        val flippedHeight = if (flip) width else height
+        targetFormat.setInteger(KEY_WIDTH, flippedWidth)
+        targetFormat.setInteger(KEY_HEIGHT, flippedHeight)
+        log.i("encoded output format: $targetFormat")
+        log.i("output size=${flippedWidth}x${flippedHeight}, flipped=$flip")
     }
 
     // VideoTrackTranscoder.onConfigureDecoder
     override fun handleSourceFormat(sourceFormat: MediaFormat): Surface {
-        log.i("handleSourceFormat($sourceFormat)")
+        log.i("encoded input format: $sourceFormat")
 
         // Just a sanity check that the rotation coming from DataSource is not different from
         // the one found in the DataSource's MediaFormat for video.
@@ -89,9 +89,11 @@ internal class VideoRenderer(
         return frameDrawer.surface
     }
 
-    override fun handleRawFormat(rawFormat: MediaFormat) = Unit
+    override fun handleRawFormat(rawFormat: MediaFormat) {
+        log.i("decoded input format: $rawFormat")
+    }
 
-    override fun step(state: State.Ok<DecoderData>, fresh: Boolean): State<Long> {
+    override fun advance(state: State.Ok<DecoderData>): State<Long> {
         return if (state is State.Eos) {
             state.value.release(false)
             State.Eos(0L)
@@ -102,7 +104,7 @@ internal class VideoRenderer(
                 State.Ok(state.value.timeUs)
             } else {
                 state.value.release(false)
-                State.Wait
+                State.Consume()
             }
         }
     }
@@ -110,4 +112,4 @@ internal 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 e8ba3e35..09fd5c28 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
@@ -20,13 +20,12 @@ import java.nio.ByteOrder
 import kotlin.math.abs
 
 internal class VideoSnapshots(
-        format: MediaFormat,
-        requests: List<Long>,
-        private val accuracyUs: Long,
-        private val onSnapshot: (Long, Bitmap) -> Unit
-) : BaseStep<Long, Channel, Long, Channel>() {
+    format: MediaFormat,
+    requests: List<Long>,
+    private val accuracyUs: Long,
+    private val onSnapshot: (Long, Bitmap) -> Unit
+) : BaseStep<Long, Channel, Long, Channel>("VideoSnapshots") {
 
-    private val log = Logger("VideoSnapshots")
     override val channel = Channel
     private val requests = requests.toMutableList()
     private val width = format.getInteger(KEY_WIDTH)
@@ -36,7 +35,7 @@ internal class VideoSnapshots(
         it.makeCurrent()
     }
 
-    override fun step(state: State.Ok<Long>, fresh: Boolean): State<Long> {
+    override fun advance(state: State.Ok<Long>): State<Long> {
         if (requests.isEmpty()) return state
 
         val expectedUs = requests.first()
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/sink/DefaultDataSink.java b/lib/src/main/java/com/otaliastudios/transcoder/sink/DefaultDataSink.java
index b17446e7..5d8d2ed1 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/sink/DefaultDataSink.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/sink/DefaultDataSink.java
@@ -12,6 +12,8 @@
 import com.otaliastudios.transcoder.common.TrackType;
 import com.otaliastudios.transcoder.internal.utils.MutableTrackMap;
 import com.otaliastudios.transcoder.internal.utils.Logger;
+import com.otaliastudios.transcoder.time.MonotonicTimeInterpolator;
+import com.otaliastudios.transcoder.time.TimeInterpolator;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -37,13 +39,17 @@ public class DefaultDataSink implements DataSink {
      */
     private static class QueuedSample {
         private final TrackType mType;
+        private ByteBuffer mByteBuffer;
+
         private final int mSize;
         private final long mTimeUs;
         private final int mFlags;
 
         private QueuedSample(@NonNull TrackType type,
+                             @NonNull ByteBuffer byteBuffer,
                              @NonNull MediaCodec.BufferInfo bufferInfo) {
             mType = type;
+            mByteBuffer = byteBuffer;
             mSize = bufferInfo.size;
             mTimeUs = bufferInfo.presentationTimeUs;
             mFlags = bufferInfo.flags;
@@ -52,18 +58,14 @@ private QueuedSample(@NonNull TrackType type,
 
     private final static Logger LOG = new Logger("DefaultDataSink");
 
-    // We must be able to handle potentially big buffers (e.g. first keyframe) in the queue.
-    // Got crashes with 152kb - let's use 256kb. TODO use a dynamic queue instead
-    private final static int BUFFER_SIZE = 256 * 1024;
-
     private boolean mMuxerStarted = false;
     private final MediaMuxer mMuxer;
     private final List<QueuedSample> mQueue = new ArrayList<>();
-    private ByteBuffer mQueueBuffer;
     private final MutableTrackMap<TrackStatus> mStatus = mutableTrackMapOf(null);
     private final MutableTrackMap<MediaFormat> mLastFormat = mutableTrackMapOf(null);
     private final MutableTrackMap<Integer> mMuxerIndex = mutableTrackMapOf(null);
     private final DefaultDataSinkChecks mMuxerChecks = new DefaultDataSinkChecks();
+    private final TimeInterpolator mInterpolator = new MonotonicTimeInterpolator();
 
     public DefaultDataSink(@NonNull String outputFilePath) {
         this(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
@@ -100,9 +102,7 @@ public void setOrientation(int rotation) {
 
     @Override
     public void setLocation(double latitude, double longitude) {
-        if (Build.VERSION.SDK_INT >= 19) {
-            mMuxer.setLocation((float) latitude, (float) longitude);
-        }
+        mMuxer.setLocation((float) latitude, (float) longitude);
     }
 
     @Override
@@ -151,6 +151,9 @@ private void maybeStart() {
     @Override
     public void writeTrack(@NonNull TrackType type, @NonNull ByteBuffer byteBuffer, @NonNull MediaCodec.BufferInfo bufferInfo) {
         if (mMuxerStarted) {
+            if (bufferInfo.presentationTimeUs != 0) {
+                bufferInfo.presentationTimeUs = mInterpolator.interpolate(type, bufferInfo.presentationTimeUs);
+            }
             /* LOG.v("writeTrack(" + type + "): offset=" + bufferInfo.offset
                     + "\trealOffset=" + byteBuffer.position()
                     + "\tsize=" + bufferInfo.size
@@ -177,19 +180,14 @@ public void writeTrack(@NonNull TrackType type, @NonNull ByteBuffer byteBuffer,
     private void enqueue(@NonNull TrackType type,
                          @NonNull ByteBuffer buffer,
                          @NonNull MediaCodec.BufferInfo bufferInfo) {
-        if (mQueueBuffer == null) {
-            mQueueBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE).order(ByteOrder.nativeOrder());
-        }
         LOG.v("enqueue(" + type + "): offset=" + bufferInfo.offset
                 + "\trealOffset=" + buffer.position()
                 + "\tsize=" + bufferInfo.size
-                + "\trealSize=" + buffer.remaining()
-                + "\tavailable=" + mQueueBuffer.remaining()
-                + "\ttotal=" + BUFFER_SIZE);
-        buffer.limit(bufferInfo.offset + bufferInfo.size);
-        buffer.position(bufferInfo.offset);
-        mQueueBuffer.put(buffer);
-        mQueue.add(new QueuedSample(type, bufferInfo));
+                + "\trealSize=" + buffer.remaining());
+
+        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bufferInfo.size).order(ByteOrder.nativeOrder());
+        byteBuffer.put(buffer);
+        mQueue.add(new QueuedSample(type, byteBuffer, bufferInfo));
     }
 
     /**
@@ -198,19 +196,16 @@ private void enqueue(@NonNull TrackType type,
      */
     private void drainQueue() {
         if (mQueue.isEmpty()) return;
-        mQueueBuffer.flip();
         LOG.i("Output format determined, writing pending data into the muxer. "
-                + "samples:" + mQueue.size() + " "
-                + "bytes:" + mQueueBuffer.limit());
+                + "samples:" + mQueue.size());
         MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
-        int offset = 0;
         for (QueuedSample sample : mQueue) {
-            bufferInfo.set(offset, sample.mSize, sample.mTimeUs, sample.mFlags);
-            writeTrack(sample.mType, mQueueBuffer, bufferInfo);
-            offset += sample.mSize;
+            bufferInfo.set(0, sample.mSize, sample.mTimeUs, sample.mFlags);
+            sample.mByteBuffer.position(0);
+            writeTrack(sample.mType, sample.mByteBuffer, bufferInfo);
+            sample.mByteBuffer = null;
         }
         mQueue.clear();
-        mQueueBuffer = null;
     }
 
     @Override
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/AssetFileDescriptorDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/AssetFileDescriptorDataSource.java
new file mode 100644
index 00000000..c0eb3480
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/AssetFileDescriptorDataSource.java
@@ -0,0 +1,22 @@
+package com.otaliastudios.transcoder.source;
+
+import android.content.res.AssetFileDescriptor;
+import android.media.MediaExtractor;
+
+import androidx.annotation.NonNull;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * It is the caller responsibility to close the file descriptor.
+ */
+public class AssetFileDescriptorDataSource extends DataSourceWrapper {
+    public AssetFileDescriptorDataSource(@NonNull AssetFileDescriptor assetFileDescriptor) {
+        super(new FileDescriptorDataSource(
+                assetFileDescriptor.getFileDescriptor(),
+                assetFileDescriptor.getStartOffset(),
+                assetFileDescriptor.getDeclaredLength()
+        ));
+    }
+}
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..368a340c 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSourceWrapper.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSourceWrapper.java
@@ -15,18 +15,28 @@
  */
 public class DataSourceWrapper implements DataSource {
 
-    private final DataSource mSource;
+    private DataSource mSource;
 
     @SuppressWarnings("WeakerAccess")
     protected DataSourceWrapper(@NonNull DataSource source) {
         mSource = source;
     }
 
+    // Only use if you know what you are doing
+    protected DataSourceWrapper() {
+        mSource = null;
+    }
+
     @NonNull
     protected DataSource getSource() {
         return mSource;
     }
 
+    // Only use if you know what you are doing
+    protected void setSource(@NonNull DataSource source) {
+        mSource = source;
+    }
+
     @Override
     public int getOrientation() {
         return mSource.getOrientation();
@@ -86,13 +96,16 @@ public void releaseTrack(@NonNull TrackType type) {
 
     @Override
     public boolean isInitialized() {
-        return mSource.isInitialized();
+        return mSource != null && mSource.isInitialized();
     }
 
     @Override
     public void initialize() {
         // Make it easier for subclasses to put their logic in initialize safely.
-        if (!mSource.isInitialized()) {
+        if (!isInitialized()) {
+            if (mSource == null) {
+                throw new NullPointerException("DataSourceWrapper's source is not set!");
+            }
             mSource.initialize();
         }
     }
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..5558b6bf 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java
@@ -66,12 +66,6 @@ public void initialize() {
             }
         }
 
-        // Fetch the start timestamp. Only way to do this is select tracks.
-        // This is very important to have a timebase e.g. for seeks that happen before any read.
-        for (int i = 0; i < mExtractor.getTrackCount(); i++) mExtractor.selectTrack(i);
-        mOriginUs = mExtractor.getSampleTime();
-        LOG.v("initialize(): found origin=" + mOriginUs);
-        for (int i = 0; i < mExtractor.getTrackCount(); i++) mExtractor.unselectTrack(i);
         mInitialized = true;
 
         // Debugging mOriginUs issues.
@@ -88,6 +82,18 @@ public void initialize() {
         } */
     }
 
+    /**
+     * Some properties are better initialized lazily instead of during initialize().
+     * For example, the origin - very important to have a timebase before reads and seeks -
+     * requires the extractor tracks to be selected, which is not true in initialize.
+     * Selecting and unselecting all tracks is not correct either.
+     */
+    private void initializeLazyProperties() {
+        if (mOriginUs == Long.MIN_VALUE) {
+            mOriginUs = mExtractor.getSampleTime();
+        }
+    }
+
     @Override
     public void deinitialize() {
         LOG.i("deinitialize(): deinitializing...");
@@ -141,6 +147,8 @@ public void releaseTrack(@NonNull TrackType type) {
 
     @Override
     public long seekTo(long desiredPositionUs) {
+        initializeLazyProperties();
+
         boolean hasVideo = mSelectedTracks.contains(TrackType.VIDEO);
         boolean hasAudio = mSelectedTracks.contains(TrackType.AUDIO);
         LOG.i("seekTo(): seeking to " + (mOriginUs + desiredPositionUs)
@@ -193,6 +201,8 @@ public boolean canReadTrack(@NonNull TrackType type) {
 
     @Override
     public void readTrack(@NonNull Chunk chunk) {
+        initializeLazyProperties();
+
         int index = mExtractor.getSampleTrackIndex();
 
         int position = chunk.buffer.position();
@@ -236,7 +246,7 @@ public void readTrack(@NonNull Chunk chunk) {
 
     @Override
     public long getPositionUs() {
-        if (!isInitialized()) return 0;
+        if (mOriginUs == Long.MIN_VALUE) return 0;
 
         // Return the fastest track.
         // This ensures linear behavior over time: if a track is behind the other,
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/FileDescriptorDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/FileDescriptorDataSource.java
index b1801bdb..0ea15158 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/FileDescriptorDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/FileDescriptorDataSource.java
@@ -1,5 +1,6 @@
 package com.otaliastudios.transcoder.source;
 
+import android.content.res.AssetFileDescriptor;
 import android.media.MediaExtractor;
 import android.media.MediaMetadataRetriever;
 
@@ -10,23 +11,33 @@
 
 /**
  * A {@link DataSource} backed by a file descriptor.
+ * It is the caller responsibility to close the file descriptor.
  */
 public class FileDescriptorDataSource extends DefaultDataSource {
 
     @NonNull
     private final FileDescriptor descriptor;
+    private final long offset;
+    private final long length;
 
     public FileDescriptorDataSource(@NonNull FileDescriptor descriptor) {
+        // length is intentionally less than LONG_MAX, see retriever
+        this(descriptor, 0, 0x7ffffffffffffffL);
+    }
+
+    public FileDescriptorDataSource(@NonNull FileDescriptor descriptor, long offset, long length) {
         this.descriptor = descriptor;
+        this.offset = offset;
+        this.length = length > 0 ? length : 0x7ffffffffffffffL;
     }
 
     @Override
     protected void initializeExtractor(@NonNull MediaExtractor extractor) throws IOException  {
-        extractor.setDataSource(descriptor);
+        extractor.setDataSource(descriptor, offset, length);
     }
 
     @Override
     protected void initializeRetriever(@NonNull MediaMetadataRetriever retriever) {
-        retriever.setDataSource(descriptor);
+        retriever.setDataSource(descriptor, offset, length);
     }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/FilePathDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/FilePathDataSource.java
index dcdd55fd..e7ef0aa3 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/source/FilePathDataSource.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/source/FilePathDataSource.java
@@ -1,9 +1,6 @@
 package com.otaliastudios.transcoder.source;
 
 import android.media.MediaExtractor;
-import android.media.MediaMetadataRetriever;
-
-import com.otaliastudios.transcoder.internal.utils.Logger;
 
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -19,10 +16,7 @@
  *
  * See {@link MediaExtractor#setDataSource(String)} documentation.
  */
-public class FilePathDataSource extends DefaultDataSource {
-    private static final Logger LOG = new Logger("FilePathDataSource");
-
-    private FileDescriptorDataSource mDescriptorSource;
+public class FilePathDataSource extends DataSourceWrapper {
     private FileInputStream mStream;
     private final String mPath;
 
@@ -34,7 +28,7 @@ public FilePathDataSource(@NonNull String path) {
     public void initialize() {
         try {
             mStream = new FileInputStream(mPath);
-            mDescriptorSource = new FileDescriptorDataSource(mStream.getFD());
+            setSource(new FileDescriptorDataSource(mStream.getFD()));
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
@@ -43,20 +37,7 @@ public void initialize() {
 
     @Override
     public void deinitialize() {
-        // I think we must recreate the stream to restart reading from the very first bytes.
-        // This means that we must also recreate the underlying source.
-        mDescriptorSource.deinitialize();
         try { mStream.close(); } catch (IOException ignore) { }
         super.deinitialize();
     }
-
-    @Override
-    protected void initializeExtractor(@NonNull MediaExtractor extractor) throws IOException {
-        mDescriptorSource.initializeExtractor(extractor);
-    }
-
-    @Override
-    protected void initializeRetriever(@NonNull MediaMetadataRetriever retriever) {
-        mDescriptorSource.initializeRetriever(retriever);
-    }
 }
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/time/MonotonicTimeInterpolator.kt b/lib/src/main/java/com/otaliastudios/transcoder/time/MonotonicTimeInterpolator.kt
new file mode 100644
index 00000000..0f230976
--- /dev/null
+++ b/lib/src/main/java/com/otaliastudios/transcoder/time/MonotonicTimeInterpolator.kt
@@ -0,0 +1,28 @@
+package com.otaliastudios.transcoder.time
+
+import android.media.MediaMuxer
+import com.otaliastudios.transcoder.common.TrackType
+import com.otaliastudios.transcoder.internal.utils.mutableTrackMapOf
+
+/**
+ * A [TimeInterpolator] that ensures timestamps are monotonically increasing.
+ * Timestamps can go back and forth for many reasons, like miscalculations in MediaCodec output
+ * or manually generated timestamps, or at the boundary between one data source and another.
+ *
+ * Since [MediaMuxer.writeSampleData] can throw in case of invalid timestamps, this interpolator
+ * ensures that the next timestamp is at least equal to the previous timestamp plus 1.
+ * It does no effort to preserve the input deltas, so the input stream must be as consistent as possible.
+ *
+ * For example, 20 30 40 50 10 20 30 would become 20 30 40 50 51 52 53.
+ */
+internal class MonotonicTimeInterpolator : TimeInterpolator {
+    private val last = mutableTrackMapOf(Long.MIN_VALUE, Long.MIN_VALUE)
+    override fun interpolate(type: TrackType, time: Long): Long {
+        return interpolate(last[type], time).also { last[type] = it }
+    }
+    private fun interpolate(prev: Long, next: Long): Long {
+        if (prev == Long.MIN_VALUE) return next
+        return next.coerceAtLeast(prev + 1)
+    }
+
+}
\ No newline at end of file
diff --git a/lib/src/main/java/com/otaliastudios/transcoder/time/SpeedTimeInterpolator.java b/lib/src/main/java/com/otaliastudios/transcoder/time/SpeedTimeInterpolator.java
index 1649665d..2e960e5b 100644
--- a/lib/src/main/java/com/otaliastudios/transcoder/time/SpeedTimeInterpolator.java
+++ b/lib/src/main/java/com/otaliastudios/transcoder/time/SpeedTimeInterpolator.java
@@ -38,7 +38,7 @@ public SpeedTimeInterpolator(float factor) {
      * @return the factor
      */
     @SuppressWarnings("unused")
-    public float getFactor() {
+    public float getFactor(@NonNull TrackType type, long time) {
         return (float) mFactor;
     }
 
@@ -50,7 +50,7 @@ public long interpolate(@NonNull TrackType type, long time) {
             data.lastCorrectedTime = time;
         } else {
             long realDelta = time - data.lastRealTime;
-            long correctedDelta = (long) ((double) realDelta / mFactor);
+            long correctedDelta = (long) ((double) realDelta / getFactor(type, time));
             data.lastRealTime = time;
             data.lastCorrectedTime += correctedDelta;
         }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index ea9f578b..4e474bb4 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1 +1,19 @@
-include(":lib", ":demo")
+pluginManagement {
+    repositories {
+        mavenLocal()
+        google()
+        gradlePluginPortal()
+        mavenCentral()
+    }
+}
+
+dependencyResolutionManagement {
+    repositories {
+        mavenCentral()
+        google()
+    }
+}
+
+include(":lib")
+include(":lib-legacy")
+include(":demo")
