├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
└── workflows
│ ├── build.yml
│ ├── deploy.yml
│ ├── emulator_script.sh
│ └── snapshot.yml
├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── gradle.xml
├── kotlinc.xml
├── misc.xml
├── runConfigurations
│ └── deployLocal.xml
└── vcs.xml
├── LICENSE
├── NOTICE
├── README.md
├── assets
└── logo-256.png
├── build.gradle.kts
├── demo
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-web.png
│ ├── java
│ └── com
│ │ └── otaliastudios
│ │ └── transcoder
│ │ └── demo
│ │ ├── ThumbnailerActivity.java
│ │ └── TranscoderActivity.java
│ └── res
│ ├── layout
│ ├── activity_thumbnailer.xml
│ └── activity_transcoder.xml
│ ├── mipmap-anydpi-v26
│ └── ic_launcher.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ └── ic_launcher_foreground.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ └── ic_launcher_foreground.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ └── ic_launcher_foreground.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ └── ic_launcher_foreground.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ └── ic_launcher_foreground.png
│ ├── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
│ └── xml
│ └── file_paths.xml
├── docs
├── README.md
├── advanced-options.mdx
├── changelog.mdx
├── clipping.mdx
├── concatenation.mdx
├── data-sources.mdx
├── events.mdx
├── index.mdx
├── install.mdx
├── track-strategies.mdx
└── validators.mdx
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── lib-legacy
├── .gitignore
└── build.gradle.kts
├── lib
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ ├── assets
│ │ ├── issue_102
│ │ │ └── sample.mp4
│ │ ├── issue_137
│ │ │ ├── 0.amr
│ │ │ ├── 1.amr
│ │ │ ├── 2.amr
│ │ │ ├── 3.amr
│ │ │ ├── 4.amr
│ │ │ ├── 5.amr
│ │ │ ├── 6.amr
│ │ │ ├── 7.amr
│ │ │ ├── 8.amr
│ │ │ └── main.mp3
│ │ ├── issue_180
│ │ │ └── party.mp4
│ │ ├── issue_184
│ │ │ └── transcode.3gp
│ │ └── issue_75
│ │ │ └── bbb_720p_30mb.mp4
│ └── java
│ │ └── com
│ │ └── otaliastudios
│ │ └── transcoder
│ │ ├── integration
│ │ └── IssuesTests.kt
│ │ └── internal
│ │ └── utils
│ │ └── ISO6709LocationParserTest.java
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── otaliastudios
│ └── transcoder
│ ├── Thumbnailer.kt
│ ├── ThumbnailerListener.kt
│ ├── ThumbnailerOptions.kt
│ ├── Transcoder.java
│ ├── TranscoderListener.java
│ ├── TranscoderOptions.java
│ ├── common
│ ├── ExactSize.java
│ ├── Size.java
│ ├── TrackStatus.java
│ └── TrackType.kt
│ ├── internal
│ ├── Codecs.kt
│ ├── DataSources.kt
│ ├── Segment.kt
│ ├── Segments.kt
│ ├── Timer.kt
│ ├── Tracks.kt
│ ├── audio
│ │ ├── AudioEngine.kt
│ │ ├── chunks.kt
│ │ ├── conversions.kt
│ │ ├── remix
│ │ │ ├── AudioRemixer.kt
│ │ │ ├── DownMixAudioRemixer.java
│ │ │ ├── PassThroughAudioRemixer.java
│ │ │ └── UpMixAudioRemixer.java
│ │ └── shorts.kt
│ ├── codec
│ │ ├── Decoder.kt
│ │ ├── DecoderDropper.kt
│ │ ├── DecoderTimer.kt
│ │ └── Encoder.kt
│ ├── data
│ │ ├── Bridge.kt
│ │ ├── Reader.kt
│ │ ├── ReaderTimer.kt
│ │ ├── Seeker.kt
│ │ └── Writer.kt
│ ├── media
│ │ ├── MediaFormatConstants.java
│ │ └── MediaFormatProvider.java
│ ├── pipeline
│ │ ├── Pipeline.kt
│ │ ├── State.kt
│ │ ├── Step.kt
│ │ ├── pipelines.kt
│ │ └── steps.kt
│ ├── thumbnails
│ │ ├── DefaultThumbnailsEngine.kt
│ │ ├── ThumbnailsDispatcher.java
│ │ └── ThumbnailsEngine.kt
│ ├── transcode
│ │ ├── DefaultTranscodeEngine.kt
│ │ ├── TranscodeDispatcher.java
│ │ └── TranscodeEngine.kt
│ ├── utils
│ │ ├── AvcCsdUtils.java
│ │ ├── AvcSpsUtils.java
│ │ ├── BitRates.java
│ │ ├── ISO6709LocationParser.java
│ │ ├── Logger.java
│ │ ├── ThreadPool.kt
│ │ ├── TrackMap.kt
│ │ ├── debug.kt
│ │ └── eos.kt
│ └── video
│ │ ├── FrameDrawer.java
│ │ ├── FrameDropper.kt
│ │ ├── VideoPublisher.kt
│ │ ├── VideoRenderer.kt
│ │ └── VideoSnapshots.kt
│ ├── resample
│ ├── AudioResampler.java
│ ├── DefaultAudioResampler.java
│ ├── DownsampleAudioResampler.java
│ ├── PassThroughAudioResampler.java
│ └── UpsampleAudioResampler.java
│ ├── resize
│ ├── AspectRatioResizer.java
│ ├── AtMostResizer.java
│ ├── ExactResizer.java
│ ├── FractionResizer.java
│ ├── MultiResizer.java
│ ├── PassThroughResizer.java
│ └── Resizer.java
│ ├── sink
│ ├── DataSink.java
│ ├── DefaultDataSink.java
│ ├── DefaultDataSinkChecks.java
│ ├── InvalidOutputFormatException.java
│ └── MultiDataSink.java
│ ├── source
│ ├── AssetFileDescriptorDataSource.java
│ ├── BlankAudioDataSource.java
│ ├── ClipDataSource.java
│ ├── DataSource.java
│ ├── DataSourceWrapper.java
│ ├── DefaultDataSource.java
│ ├── FileDescriptorDataSource.java
│ ├── FilePathDataSource.java
│ ├── TrimDataSource.java
│ └── UriDataSource.java
│ ├── strategy
│ ├── DefaultAudioStrategy.java
│ ├── DefaultVideoStrategies.java
│ ├── DefaultVideoStrategy.java
│ ├── PassThroughTrackStrategy.java
│ ├── RemoveTrackStrategy.java
│ └── TrackStrategy.java
│ ├── stretch
│ ├── AudioStretcher.java
│ ├── CutAudioStretcher.java
│ ├── DefaultAudioStretcher.java
│ ├── InsertAudioStretcher.java
│ └── PassThroughAudioStretcher.java
│ ├── thumbnail
│ ├── CoverThumbnailRequest.kt
│ ├── SingleThumbnailRequest.kt
│ ├── Thumbnail.kt
│ ├── ThumbnailRequest.kt
│ └── UniformThumbnailRequest.kt
│ ├── time
│ ├── DefaultTimeInterpolator.java
│ ├── MonotonicTimeInterpolator.kt
│ ├── SpeedTimeInterpolator.java
│ └── TimeInterpolator.java
│ └── validator
│ ├── DefaultValidator.java
│ ├── Validator.java
│ ├── WriteAlwaysValidator.java
│ └── WriteVideoValidator.java
└── settings.gradle.kts
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | 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.
38 |
39 | 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.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 | Everyone is welcome to contribute with suggestions or pull requests. We are grateful to anyone who will contribute with fixes, features or feature requests.
3 |
4 | ### Bug reports
5 |
6 | Please make sure to fill the bug report issue template on GitHub, if applicable.
7 | We highly recommend to try to reproduce the bug in the demo app, as this helps a lot in debugging
8 | and excludes programming errors from your side.
9 |
10 | Make sure to include:
11 |
12 | - A clear and concise description of what the bug is
13 | - Transcoder version, device type, Android API level
14 | - Exact steps to reproduce the issue
15 | - Description of the expected behavior
16 | - The original media file(s) that manifest the problem
17 |
18 | Recommended extras:
19 |
20 | - LogCat logs (use `Logger.setLogLevel(LEVEL_VERBOSE)` to print all)
21 | - Link to a GitHub repo where the bug is reproducible
22 |
23 | ### Pull Requests
24 |
25 | Please open an issue first!
26 |
27 | Unless your PR is a simple fix (typos, documentation, bugs with obvious solution), opening an issue
28 | will let us discuss the problem, take design decisions and have a reference to the issue description.
29 |
30 | If you can, please write tests. We are planning to work on improving the library test coverage soon.
31 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
2 | name: Build
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | jobs:
9 | ANDROID_BASE_CHECKS:
10 | name: Base Checks
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-java@v4
15 | with:
16 | java-version: 17
17 | distribution: temurin
18 | cache: gradle
19 | - name: Perform base checks
20 | run: ./gradlew demo:assembleDebug lib:deployLocal --stacktrace
21 | ANDROID_EMULATOR_TESTS:
22 | name: Emulator Tests
23 | runs-on: ubuntu-latest
24 | strategy:
25 | fail-fast: false
26 | matrix:
27 | EMULATOR_API: [24, 27, 29, 31, 34]
28 | steps:
29 | - uses: actions/checkout@v4
30 | - uses: actions/setup-java@v4
31 | with:
32 | java-version: 17
33 | distribution: temurin
34 | cache: gradle
35 |
36 | - name: Enable KVM group perms
37 | run: |
38 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
39 | sudo udevadm control --reload-rules
40 | sudo udevadm trigger --name-match=kvm
41 |
42 | - name: Execute emulator tests
43 | timeout-minutes: 30
44 | uses: reactivecircus/android-emulator-runner@v2
45 | with:
46 | api-level: ${{ matrix.EMULATOR_API }}
47 | arch: x86_64
48 | profile: Nexus 6
49 | emulator-options: -no-snapshot -no-window -no-boot-anim -camera-back none -camera-front none -gpu swiftshader_indirect
50 | script: ./.github/workflows/emulator_script.sh logcat_${{ matrix.EMULATOR_API }}.txt
51 |
52 | - name: Upload emulator logs
53 | uses: actions/upload-artifact@v4
54 | if: always()
55 | with:
56 | name: emulator_logs_${{ matrix.EMULATOR_API }}
57 | path: ./logcat_${{ matrix.EMULATOR_API }}.txt
58 |
59 | - name: Upload emulator tests artifact
60 | uses: actions/upload-artifact@v4
61 | if: always()
62 | with:
63 | name: emulator_tests_${{ matrix.EMULATOR_API }}
64 | path: ./lib/build/reports/androidTests/connected/debug/
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
2 | name: Deploy
3 | on:
4 | release:
5 | types: [published]
6 | jobs:
7 | MAVEN_UPLOAD:
8 | name: Maven Central Upload
9 | runs-on: ubuntu-latest
10 | env:
11 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
12 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
13 | SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
14 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
15 | GHUB_USER: ${{ secrets.GHUB_USER }}
16 | GHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GHUB_PERSONAL_ACCESS_TOKEN }}
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: actions/setup-java@v4
20 | with:
21 | java-version: 17
22 | distribution: temurin
23 | cache: gradle
24 | - name: Publish to Maven Central
25 | run: ./gradlew deployNexus --stacktrace
26 | - name: Publish to GitHub Packages
27 | run: ./gradlew deployGithub --stacktrace
28 |
--------------------------------------------------------------------------------
/.github/workflows/emulator_script.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | adb logcat -c
3 | adb logcat *:V > "$1" &
4 | LOGCAT_PID=$!
5 | trap "kill $LOGCAT_PID" EXIT
6 | ./gradlew lib:connectedCheck --stacktrace
--------------------------------------------------------------------------------
/.github/workflows/snapshot.yml:
--------------------------------------------------------------------------------
1 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
2 | # Renaming ? Change the README badge.
3 | name: Snapshot
4 | on:
5 | push:
6 | branches:
7 | - main
8 | jobs:
9 | SNAPSHOT:
10 | name: Publish Snapshot
11 | runs-on: ubuntu-latest
12 | env:
13 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
14 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
15 | SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
16 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
17 | GHUB_USER: ${{ secrets.GHUB_USER }}
18 | GHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GHUB_PERSONAL_ACCESS_TOKEN }}
19 | steps:
20 | - uses: actions/checkout@v4
21 | - uses: actions/setup-java@v4
22 | with:
23 | java-version: 17
24 | distribution: temurin
25 | cache: gradle
26 | - name: Publish nexus snapshot
27 | run: ./gradlew deployNexusSnapshot --stacktrace
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | .DS_Store
4 | /build
5 | *.iml
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | androidTestResultsUserPreferences.xml
5 | deploymentTargetDropDown.xml
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/deployLocal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 | false
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | android-transcoder
2 | Copyright (C) 2014-2015 Yuya Tanaka
3 |
4 | This product includes software developed by
5 | Yuya Tanaka (http://ypresto.net/).
6 |
7 | This software contains code derived from
8 | Android Compatibility Test Suite
9 | (https://android.googlesource.com/platform/cts/), which is developed by
10 | The Android Open Source Project (https://source.android.com/) and
11 | is available under a "Apache License 2.0".
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/deepmedia/Transcoder/actions)
2 | [](https://github.com/deepmedia/Transcoder/releases)
3 | [](https://github.com/deepmedia/Transcoder/issues)
4 |
5 | 
6 |
7 | # Transcoder
8 |
9 | Transcodes and compresses video files into the MP4 format, with audio support, using hardware-accelerated
10 | Android codecs available on the device. Works on API 21+.
11 |
12 | - Fast transcoding to AAC/AVC
13 | - Hardware accelerated
14 | - Convenient, fluent API
15 | - Thumbnails support
16 | - Concatenate multiple video and audio tracks [[docs]](https://opensource.deepmedia.io/transcoder/concatenation)
17 | - Clip or trim video segments [[docs]](https://opensource.deepmedia.io/transcoder/clipping)
18 | - Choose output size, with automatic cropping [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies#video-size)
19 | - Choose output rotation [[docs]](https://opensource.deepmedia.io/transcoder/advanced-options#video-rotation)
20 | - Choose output speed [[docs]](https://opensource.deepmedia.io/transcoder/advanced-options#video-speed)
21 | - Choose output frame rate [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies#other-options)
22 | - Choose output audio channels [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies#audio-strategies)
23 | - Choose output audio sample rate [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies#audio-strategies)
24 | - Override frames timestamp, e.g. to slow down the middle part of the video [[docs]](https://opensource.deepmedia.io/transcoder/advanced-options#time-interpolation)
25 | - Error handling [[docs]](https://opensource.deepmedia.io/transcoder/events)
26 | - Configurable validators to e.g. avoid transcoding if the source is already compressed enough [[docs]](https://opensource.deepmedia.io/transcoder/validators)
27 | - Configurable video and audio strategies [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies)
28 |
29 | ```kotlin
30 | // build.gradle.kts
31 | dependencies {
32 | implementation("io.deepmedia.community:transcoder-android:0.11.2")
33 | }
34 | ```
35 |
36 | *This project started as a fork of [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder).
37 | With respect to the source project, which misses most of the functionality listed above,
38 | we have also fixed a huge number of bugs and are much less conservative when choosing options
39 | that might not be supported. The source project will always throw - for example, accepting only 16:9,
40 | AVC Baseline Profile videos - we prefer to try and let the codec fail if it wants to*.
41 |
42 | *Transcoder is trusted and supported by [ShareChat](https://sharechat.com/), a social media app with
43 | over 100 million downloads.*
44 |
45 | Please check out [the official website](https://opensource.deepmedia.io/transcoder) for setup instructions and documentation.
46 | You may also check the demo app (under `/demo`) for a complete example.
47 |
48 | ```kotlin
49 | Transcoder.into(filePath)
50 | .addDataSource(context, uri) // or...
51 | .addDataSource(filePath) // or...
52 | .addDataSource(fileDescriptor) // or...
53 | .addDataSource(dataSource)
54 | .setListener(object : TranscoderListener {
55 | override fun onTranscodeProgress(progress: Double) = Unit
56 | override fun onTranscodeCompleted(successCode: Int) = Unit
57 | override fun onTranscodeCanceled() = Unit
58 | override fun onTranscodeFailed(exception: Throwable) = Unit
59 | }).transcode()
60 | ```
61 |
--------------------------------------------------------------------------------
/assets/logo-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/assets/logo-256.png
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("android") version "2.0.0" apply false
3 | id("com.android.library") version "8.2.2" apply false
4 | id("com.android.application") version "8.2.2" apply false
5 | id("io.deepmedia.tools.deployer") version "0.14.0" apply false
6 | }
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/demo/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | kotlin("android")
4 | }
5 |
6 | android {
7 | namespace = "com.otaliastudios.transcoder.demo"
8 | compileSdk = 34
9 | defaultConfig {
10 | minSdk = 21
11 | targetSdk = 34
12 | versionCode = 1
13 | versionName = "1.0"
14 | }
15 | }
16 |
17 | dependencies {
18 | implementation(project(":lib"))
19 | implementation("com.google.android.material:material:1.12.0")
20 | implementation("androidx.appcompat:appcompat:1.7.0")
21 | }
22 |
--------------------------------------------------------------------------------
/demo/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/yuya.tanaka/devel/android-sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/demo/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
40 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/demo/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-hdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-mdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-xhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/demo/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #1E353F
4 | #0D161B
5 | #1EC4FF
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Transcoder
4 |
5 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/demo/src/main/res/xml/file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | Read the docs at https://opensource.deepmedia.io/transcoder.
2 |
--------------------------------------------------------------------------------
/docs/advanced-options.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Advanced Options
3 | ---
4 |
5 | # Advanced Options
6 |
7 | ## Video rotation
8 |
9 | You can set the output video rotation with the `setRotation(int)` method. This will apply a clockwise
10 | rotation to the input video frames. Accepted values are `0`, `90`, `180`, `270`:
11 |
12 | ```kotlin
13 | Transcoder.into(filePath)
14 | .setVideoRotation(rotation) // 0, 90, 180, 270
15 | // ...
16 | ```
17 |
18 | ## Time interpolation
19 |
20 | We offer APIs to change the timestamp of each video and audio frame. You can pass a `TimeInterpolator`
21 | to the transcoder builder to be able to receive the frame timestamp as input, and return a new one
22 | as output.
23 |
24 | ```kotlin
25 | Transcoder.into(filePath)
26 | .setTimeInterpolator(timeInterpolator)
27 | // ...
28 | ```
29 |
30 | As an example, this is the implementation of the default interpolator, called `DefaultTimeInterpolator`,
31 | that will just return the input time unchanged:
32 |
33 | ```kotlin
34 | override fun interpolate(type: TrackType, time: Long): Long {
35 | // Receive input time in microseconds and return a possibly different one.
36 | return time
37 | }
38 | ```
39 |
40 | It should be obvious that returning invalid times can make the process crash at any point, or at least
41 | the transcoding operation fail.
42 |
43 | ## Video speed
44 |
45 | We also offer a special time interpolator called `SpeedTimeInterpolator` that accepts a `float` parameter
46 | and will modify the video speed.
47 |
48 | - A speed factor equal to 1 will leave speed unchanged
49 | - A speed factor < 1 will slow the video down
50 | - A speed factor > 1 will accelerate the video
51 |
52 | This interpolator can be set using `setTimeInterpolator(TimeInterpolator)`, or, as a shorthand,
53 | using `setSpeed(float)`:
54 |
55 | ```kotlin
56 | Transcoder.into(filePath)
57 | .setSpeed(0.5F) // 0.5x
58 | .setSpeed(1F) // Unchanged
59 | .setSpeed(2F) // Twice as fast
60 | // ...
61 | ```
62 |
63 | ## Audio stretching
64 |
65 | When a time interpolator alters the frames and samples timestamps, you can either remove audio or
66 | stretch the audio samples to the new length. This is done through the `AudioStretcher` interface:
67 |
68 | ```kotlin
69 | Transcoder.into(filePath)
70 | .setAudioStretcher(audioStretcher)
71 | // ...
72 | ```
73 |
74 | The default audio stretcher, `DefaultAudioStretcher`, will:
75 |
76 | - When we need to shrink a group of samples, cut the last ones
77 | - When we need to stretch a group of samples, insert noise samples in between
78 |
79 | Please take a look at the implementation and read class documentation.
80 |
81 | ## Audio resampling
82 |
83 | When a sample rate different than the input is specified (by the `TrackStrategy`, or, when using the
84 | default audio strategy, by `DefaultAudioStrategy.Builder.sampleRate()`), this library will automatically
85 | perform sample rate conversion for you.
86 |
87 | This operation is performed by a class called `AudioResampler`. We offer the option to pass your
88 | own resamplers through the transcoder builder:
89 |
90 | ```kotlin
91 | Transcoder.into(filePath)
92 | .setAudioResampler(audioResampler)
93 | // ...
94 | ```
95 |
96 | The default audio resampler, `DefaultAudioResampler`, will perform both upsampling and downsampling
97 | with very basic algorithms (drop samples when downsampling, repeat samples when upsampling).
98 | Upsampling is generally discouraged - implementing a real upsampling algorithm is probably out of
99 | the scope of this library.
100 |
101 | Please take a look at the implementation and read class documentation.
--------------------------------------------------------------------------------
/docs/clipping.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Clip & Trim
3 | ---
4 |
5 | # Clip & Trim
6 |
7 | Starting from `v0.8.0`, Transcoder offers the option to clip or trim video segments, on one or
8 | both ends. This is done by using special `DataSource` objects that wrap you original source,
9 | so that, in case of [concatenation](concatenation) of multiple media files, the trimming values
10 | can be set individually for each segment.
11 |
12 | > If Transcoder determines that the video should be decoded and re-encoded (status is `TrackStatus.COMPRESSING`)
13 | the clipping position is respected precisely. However, if your [strategy](track-strategies) does not
14 | include video decoding / re-encoding, the clipping position will be moved to the closest video sync frame.
15 | This means that the clipped output duration might be different than expected,
16 | depending on the frequency of sync frames in your original file.
17 |
18 | ## TrimDataSource
19 |
20 | The `TrimDataSource` class lets you trim segments by specifying the amount of time to be trimmed
21 | at both ends. For example, the code below will trim the file by 1 second at the beginning, and
22 | 2 seconds at the end:
23 |
24 | ```kotlin
25 | let source: DataSource = UriDataSource(context, uri)
26 | let trim: DataSource = TrimDataSource(source, 1000 * 1000, 2 * 1000 * 1000)
27 | Transcoder.into(filePath)
28 | .addDataSource(trim)
29 | .transcode()
30 | ```
31 |
32 | It is recommended to always check `source.getDurationUs()` to compute the correct values.
33 |
34 | ## ClipDataSource
35 |
36 | The `ClipDataSource` class lets you clip segments by specifying a time window. For example,
37 | the code below clip the file from second 1 until second 5:
38 |
39 | ```kotlin
40 | let source: DataSource = UriDataSource(context, uri)
41 | let clip: DataSource = ClipDataSource(source, 1000 * 1000, 5 * 1000 * 1000)
42 | Transcoder.into(filePath)
43 | .addDataSource(clip)
44 | .transcode()
45 | ```
46 |
47 | It is recommended to always check `source.getDurationUs()` to compute the correct values.
48 |
49 | ## Related APIs
50 |
51 | |Method|Description|
52 | |------|-----------|
53 | |`TrimDataSource(source, long)`|Creates a new data source trimmed on start.|
54 | |`TrimDataSource(source, long, long)`|Creates a new data source trimmed on both ends.|
55 | |`ClipDataSource(source, long)`|Creates a new data source clipped on start.|
56 | |`ClipDataSource(source, long, long)`|Creates a new data source clipped on both ends.|
--------------------------------------------------------------------------------
/docs/concatenation.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Concatenation
3 | ---
4 |
5 | # Concatenation
6 |
7 | ## Appending sources
8 |
9 | As you might have guessed from the previous section, you can use `addDataSource(source)` multiple times.
10 | All the source files will be stitched together:
11 |
12 | ```kotlin
13 | Transcoder.into(filePath)
14 | .addDataSource(source1)
15 | .addDataSource(source2)
16 | .addDataSource(source3)
17 | // ...
18 | ```
19 |
20 | In the above example, the three videos will be stitched together in the order they are added
21 | to the builder. Once `source1` ends, we'll append `source2` and so on. The library will take care
22 | of applying consistent parameters (frame rate, bit rate, sample rate) during the conversion.
23 |
24 | This is a powerful tool since it can be used per-track:
25 |
26 | ```kotlin
27 | Transcoder.into(filePath)
28 | .addDataSource(source1) // Audio & Video, 20 seconds
29 | .addDataSource(TrackType.VIDEO, source2) // Video, 5 seconds
30 | .addDataSource(TrackType.VIDEO, source3) // Video, 5 seconds
31 | .addDataSource(TrackType.AUDIO, source4) // Audio, 10 sceonds
32 | // ...
33 | ```
34 |
35 | In the above example, the output file will be 30 seconds long:
36 |
37 | ```
38 | Video: | •••••••••••••••••• source1 •••••••••••••••••• | •••• source2 •••• | •••• source3 •••• |
39 | Audio: | •••••••••••••••••• source1 •••••••••••••••••• | •••••••••••••• source4 •••••••••••••• |
40 | ```
41 |
42 | And that's all you need to do.
43 |
44 | ## Automatic clipping
45 |
46 | When concatenating data from multiple sources and on different tracks, it's common to have
47 | a total audio length that is different than the total video length.
48 |
49 | In this case, `Transcoder` will automatically clip the longest track to match the shorter.
50 | For example:
51 |
52 | ```kotlin
53 | Transcoder.into(filePath)
54 | .addDataSource(TrackType.VIDEO, video1) // Video, 30 seconds
55 | .addDataSource(TrackType.VIDEO, video2) // Video, 30 seconds
56 | .addDataSource(TrackType.AUDIO, music) // Audio, 3 minutes
57 | // ...
58 | ```
59 |
60 | In the situation above, we won't use the full music track, but only the first minute of it.
--------------------------------------------------------------------------------
/docs/data-sources.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Data Sources
3 | ---
4 |
5 | # Data Sources
6 |
7 | Starting a transcoding operation will require a source for our data, which is not necessarily
8 | a `File`. The `DataSource` objects will automatically take care about releasing streams / resources,
9 | which is convenient but it means that they can not be used twice.
10 |
11 | ```kotlin
12 | Transcoder.into(filePath)
13 | .addDataSource(source1)
14 | .transcode()
15 | ```
16 |
17 | ## Source Types
18 |
19 | #### UriDataSource
20 |
21 | The Android friendly source can be created with `UriDataSource(context, uri)` or simply
22 | using `addDataSource(context, uri)` in the transcoding builder.
23 |
24 | #### FileDescriptorDataSource
25 |
26 | A data source backed by a file descriptor. Use `FileDescriptorDataSource(descriptor)` or
27 | simply `addDataSource(descriptor)` in the transcoding builder. Note that it is the caller
28 | responsibility to close the file descriptor.
29 |
30 | #### FilePathDataSource
31 |
32 | A data source backed by a file absolute path. Use `FilePathDataSource(path)` or
33 | simply `addDataSource(path)` in the transcoding builder.
34 |
35 | #### AssetFileDescriptorDataSource
36 |
37 | A data source backed by Android's AssetFileDescriptor. Use `AssetFileDescriptorDataSource(descriptor)`
38 | or simply `addDataSource(descriptor)` in the transcoding builder. Note that it is the caller
39 | responsibility to close the file descriptor.
40 |
41 | ## Track specific sources
42 |
43 | Although a media source can have both audio and video, you can select a specific track
44 | for transcoding and exclude the other(s). For example, to select the video track only:
45 |
46 | ```java
47 | Transcoder.into(filePath)
48 | .addDataSource(TrackType.VIDEO, source)
49 | .transcode()
50 | ```
51 |
52 | ## Related APIs
53 |
54 | |Method|Description|
55 | |------|-----------|
56 | |`addDataSource(Context, Uri)`|Adds a new source for the given Uri.|
57 | |`addDataSource(FileDescriptor)`|Adds a new source for the given FileDescriptor.|
58 | |`addDataSource(String)`|Adds a new source for the given file path.|
59 | |`addDataSource(DataSource)`|Adds a new source.|
60 | |`addDataSource(TrackType, DataSource)`|Adds a new source restricted to the given TrackType.|
--------------------------------------------------------------------------------
/docs/events.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Events
3 | ---
4 |
5 | # Events
6 |
7 | Transcoding will happen on a background thread, but we will send updates through the `TranscoderListener`
8 | interface, which can be applied when building the request:
9 |
10 | ```kotlin
11 | Transcoder.into(filePath)
12 | .setListenerHandler(handler)
13 | .setListener(object: TranscoderListener {
14 | override fun onTranscodeProgress(progress: Double) = Unit
15 | override fun onTranscodeCompleted(successCode: Int) = Unit
16 | override fun onTranscodeCanceled() = Unit
17 | override fun onTranscodeFailed(exception: Throwable) = Unit
18 | })
19 | // ...
20 | ```
21 |
22 | All of the listener callbacks are called:
23 |
24 | - If present, on the handler specified by `setListenerHandler()`
25 | - If it has a handler, on the thread that started the `transcode()` call
26 | - As a last resort, on the UI thread
27 |
28 | ### onTranscodeProgress
29 |
30 | This simply sends a double indicating the current progress. The value is typically between 0 and 1,
31 | but can be a negative value to indicate that we are not able to compute progress (yet?).
32 |
33 | This is the right place to update a ProgressBar, for example.
34 |
35 | ### onTranscodeCanceled
36 |
37 | The transcoding operation was canceled. This can happen when the `Future` returned by `transcode()`
38 | is cancelled by the user.
39 |
40 | ### onTranscodeFailed
41 |
42 | This can happen in a number of cases and is typically out of our control. Input options might be
43 | wrong, write permissions might be missing, codec might be absent, input file might be not supported
44 | or simply corrupted.
45 |
46 | You can take a look at the `Throwable` being passed to know more about the exception.
47 |
48 | ### onTranscodeCompleted
49 |
50 | Transcoding operation did succeed. The success code can be:
51 |
52 | |Code|Meaning|
53 | |----|-------|
54 | |`Transcoder.SUCCESS_TRANSCODED`|Transcoding was executed successfully. Transcoded file was written to the output path.|
55 | |`Transcoder.SUCCESS_NOT_NEEDED`|Transcoding was not executed because it was considered **not needed** by the `Validator`.|
56 |
--------------------------------------------------------------------------------
/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Intro
3 | docs:
4 | - install
5 | - changelog
6 | - data-sources
7 | - clipping
8 | - concatenation
9 | - events
10 | - validators
11 | - track-strategies
12 | - advanced-options
13 | ---
14 |
15 | # Intro
16 |
17 | The Transcoder library transcodes and compresses video files into the MP4 format, with audio support, using hardware-accelerated
18 | Android codecs available on the device. Works on API 19+ and supports the following set of features:
19 |
20 | - Fast transcoding to AAC/AVC
21 | - Hardware accelerated
22 | - Convenient, fluent API
23 | - Thumbnails support
24 | - [Concatenate](concatenation) multiple video and audio tracks
25 | - [Clip or trim](clipping) video segments
26 | - Configure [output size](track-strategies#video-size), with automatic cropping
27 | - Configure [output rotation](advanced-options#video-rotation)
28 | - Configure [output speed](advanced-options#video-speed)
29 | - Configure [output frame rate](track-strategies#other-options)
30 | - Configure [output audio channels](track-strategies#audio-strategies) and sample rate
31 | - [Override timestamp](advanced-options#time-interpolation) of frames, for example to slow down parts of the video
32 | - [Error handling](events)
33 | - Configurable [validators](validators) to e.g. avoid transcoding if the source is already compressed enough
34 | - Configurable video and audio [strategies](track-strategies)
35 |
36 | > This project started as a fork of [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder).
37 | With respect to the source project, which misses most of the functionality listed above,
38 | we have also fixed a huge number of bugs and are much less conservative when choosing options
39 | that might not be supported. The source project will always throw - for example, accepting only 16:9,
40 | AVC Baseline Profile videos - we prefer to try and let the codec fail if it wants to.
41 |
42 | ## Minimal example
43 |
44 | ```kotlin
45 | Transcoder.into(filePath)
46 | .addDataSource(context, uri) // or...
47 | .addDataSource(filePath) // or...
48 | .addDataSource(fileDescriptor) // or...
49 | .addDataSource(dataSource)
50 | .setListener(object : TranscoderListener {
51 | override fun onTranscodeProgress(progress: Double) = Unit
52 | override fun onTranscodeCompleted(successCode: Int) = Unit
53 | override fun onTranscodeCanceled() = Unit
54 | override fun onTranscodeFailed(exception: Throwable) = Unit
55 | }).transcode()
56 | ```
57 |
58 | Please keep reading the documentation to learn about [install instructions](install), configuration options and APIs.
59 |
60 |
61 | ## License
62 |
63 | This project is licensed under Apache 2.0. It consists of improvements over
64 | the [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder)
65 | project which was licensed under Apache 2.0 as well:
66 |
67 | ```
68 | Copyright (C) 2014-2016 Yuya Tanaka
69 |
70 | Licensed under the Apache License, Version 2.0 (the "License");
71 | you may not use this file except in compliance with the License.
72 | You may obtain a copy of the License at
73 |
74 | http://www.apache.org/licenses/LICENSE-2.0
75 |
76 | Unless required by applicable law or agreed to in writing, software
77 | distributed under the License is distributed on an "AS IS" BASIS,
78 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
79 | See the License for the specific language governing permissions and
80 | limitations under the License.
81 | ```
--------------------------------------------------------------------------------
/docs/install.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Install
3 | ---
4 |
5 | # Installation
6 |
7 | Transcoder is publicly hosted on the [Maven Central](https://repo1.maven.org/maven2/io/deepmedia/community/)
8 | repository, where you can download the AAR package. To fetch with Gradle, assuming that `mavenCentral()` is already
9 | one of your repository sources, simply declare a new dependency:
10 |
11 | ```kotlin
12 | dependencies {
13 | api("io.deepmedia.community:transcoder-android:LATEST_VERSION")
14 |
15 | // Or use the legacy coordinates:
16 | // api("com.otaliastudios:transcoder:LATEST_VERSION")
17 | }
18 | ```
19 |
20 | Replace `LATEST_VERSION` with the latest version number, {version}.
21 |
22 | ## Snapshots
23 |
24 | We regularly push development snapshots of the library at `https://s01.oss.sonatype.org/content/repositories/snapshots/`
25 | on each push to main. To use snapshots, add the url as a maven repository and depend on `latest-SNAPSHOT`:
26 |
27 | ```kotlin
28 | // settings.gradle.kts
29 | pluginManagement {
30 | repositories {
31 | gradlePluginPortal()
32 | maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/")
33 | }
34 | }
35 |
36 | // build.gradle.kts
37 | dependencies {
38 | api("io.deepmedia.community:transcoder-android:latest-SNAPSHOT")
39 |
40 | // Or use the legacy coordinates:
41 | // api("com.otaliastudios:transcoder:latest-SNAPSHOT")
42 | }
43 | ```
--------------------------------------------------------------------------------
/docs/validators.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Validators
3 | ---
4 |
5 | # Validators
6 |
7 | Validators tell the engine whether the transcoding process should start or not based on the status
8 | of the audio and video track.
9 |
10 | ```kotlin
11 | Transcoder.into(filePath)
12 | .setValidator(validator)
13 | // ...
14 | ```
15 |
16 | This can be used, for example, to:
17 |
18 | - avoid transcoding when video resolution is already OK with our needs
19 | - avoid operating on files without an audio/video stream
20 | - avoid operating on files with an audio/video stream
21 |
22 | Validators should implement the `validate(TrackStatus, TrackStatus)` and inspect the status for video
23 | and audio tracks. When `false` is returned, transcoding will complete with the `SUCCESS_NOT_NEEDED` status code.
24 | The TrackStatus enum contains the following values:
25 |
26 | |Value|Meaning|
27 | |-----|-------|
28 | |`TrackStatus.ABSENT`|This track was absent in the source file.|
29 | |`TrackStatus.PASS_THROUGH`|This track is about to be copied as-is in the target file.|
30 | |`TrackStatus.COMPRESSING`|This track is about to be processed and compressed in the target file.|
31 | |`TrackStatus.REMOVING`|This track will be removed in the target file.|
32 |
33 | The `TrackStatus` value depends on the [track strategy](track-strategies) that was used.
34 | We provide a few validators that can be injected for typical usage.
35 |
36 | ### DefaultValidator
37 |
38 | This is the default validator and it returns true when any of the track is `COMPRESSING` or `REMOVING`.
39 | In the other cases, transcoding is typically not needed so we abort the operation.
40 |
41 | ### WriteAlwaysValidator
42 |
43 | This validator always returns true and as such will always write to target file, no matter the track status,
44 | presence of tracks and so on. For instance, the output container file might have no tracks.
45 |
46 | ### WriteVideoValidator
47 |
48 | A Validator that gives priority to the video track. Transcoding will not happen if the video track does not need it,
49 | even if the audio track might need it. If reducing file size is your only concern, this can avoid compressing
50 | files that would not benefit so much from compressing the audio track only.
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 |
2 | android.useAndroidX=true
3 | org.gradle.caching=true
4 | org.gradle.caching.debug=false
5 | org.gradle.parallel=true
6 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jun 08 12:40:11 IST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/lib-legacy/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/lib-legacy/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import io.deepmedia.tools.deployer.model.Secret
2 |
3 | plugins {
4 | id("com.android.library")
5 | id("io.deepmedia.tools.deployer")
6 | }
7 |
8 | android {
9 | namespace = "com.otaliastudios.transcoder"
10 | compileSdk = 34
11 | defaultConfig.minSdk = 21
12 | publishing { singleVariant("release") }
13 | }
14 |
15 | dependencies {
16 | api(project(":lib"))
17 | }
18 |
19 | deployer {
20 | content {
21 | component {
22 | fromSoftwareComponent("release")
23 | emptyDocs()
24 | emptySources()
25 | }
26 | }
27 |
28 | projectInfo {
29 | groupId = "com.otaliastudios"
30 | artifactId = "transcoder"
31 | release.version = "0.11.2" // change :lib and README
32 | 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."
33 | url = "https://opensource.deepmedia.io/transcoder"
34 | scm.fromGithub("deepmedia", "Transcoder")
35 | license(apache2)
36 | developer("Mattia Iavarone", "mattia@deepmedia.io", "DeepMedia", "https://deepmedia.io")
37 | }
38 |
39 | signing {
40 | key = secret("SIGNING_KEY")
41 | password = secret("SIGNING_PASSWORD")
42 | }
43 |
44 | // use "deployLocal" to deploy to local maven repository
45 | localSpec {
46 | directory.set(rootProject.layout.buildDirectory.get().dir("inspect"))
47 | signing {
48 | key = absent()
49 | password = absent()
50 | }
51 | }
52 |
53 | // use "deployNexus" to deploy to OSSRH / maven central
54 | nexusSpec {
55 | auth.user = secret("SONATYPE_USER")
56 | auth.password = secret("SONATYPE_PASSWORD")
57 | syncToMavenCentral = true
58 | }
59 |
60 | // use "deployNexusSnapshot" to deploy to sonatype snapshots repo
61 | nexusSpec("snapshot") {
62 | auth.user = secret("SONATYPE_USER")
63 | auth.password = secret("SONATYPE_PASSWORD")
64 | repositoryUrl = ossrhSnapshots1
65 | release.version = "latest-SNAPSHOT"
66 | }
67 |
68 | // use "deployGithub" to deploy to github packages
69 | githubSpec {
70 | repository = "Transcoder"
71 | owner = "deepmedia"
72 | auth {
73 | user = secret("GHUB_USER")
74 | token = secret("GHUB_PERSONAL_ACCESS_TOKEN")
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/lib/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/lib/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import io.deepmedia.tools.deployer.model.Secret
2 | import org.gradle.api.publish.maven.internal.publication.MavenPublicationInternal
3 |
4 | plugins {
5 | id("com.android.library")
6 | kotlin("android")
7 | id("io.deepmedia.tools.deployer")
8 | id("org.jetbrains.dokka") version "1.9.20"
9 | }
10 |
11 | android {
12 | namespace = "io.deepmedia.transcoder"
13 | compileSdk = 34
14 | defaultConfig {
15 | minSdk = 21
16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 | testOptions {
19 | targetSdk = 23
20 | }
21 | publishing {
22 | singleVariant("release")
23 | }
24 | }
25 |
26 | kotlin {
27 | jvmToolchain(17)
28 | }
29 |
30 | dependencies {
31 | api("com.otaliastudios.opengl:egloo:0.6.1")
32 | api("androidx.annotation:annotation:1.8.2")
33 |
34 | androidTestImplementation("androidx.test:runner:1.6.1")
35 | androidTestImplementation("androidx.test:rules:1.6.1")
36 | androidTestImplementation("androidx.test.ext:junit:1.2.1")
37 | androidTestImplementation("org.mockito:mockito-android:2.28.2")
38 |
39 | dokkaPlugin("org.jetbrains.dokka:android-documentation-plugin:1.9.20")
40 | }
41 |
42 | val javadocs = tasks.register("dokkaJavadocJar") {
43 | dependsOn(tasks.dokkaJavadoc)
44 | from(tasks.dokkaJavadoc.flatMap { it.outputDirectory })
45 | archiveClassifier.set("javadoc")
46 | }
47 |
48 | // Ugly workaround because the snapshot publication has different version and maven-publish
49 | // is then unable to determine the right coordinates for lib-legacy dependency on this project
50 | publishing.publications.withType().configureEach {
51 | isAlias = name != "localReleaseComponent"
52 | }
53 |
54 | deployer {
55 | content {
56 | component {
57 | fromSoftwareComponent("release")
58 | kotlinSources()
59 | docs(javadocs)
60 | }
61 | }
62 |
63 | projectInfo {
64 | groupId = "io.deepmedia.community"
65 | artifactId = "transcoder-android"
66 | release.version = "0.11.2" // change :lib-legacy and README
67 | 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."
68 | url = "https://opensource.deepmedia.io/transcoder"
69 | scm.fromGithub("deepmedia", "Transcoder")
70 | license(apache2)
71 | developer("Mattia Iavarone", "mattia@deepmedia.io", "DeepMedia", "https://deepmedia.io")
72 | }
73 |
74 | signing {
75 | key = secret("SIGNING_KEY")
76 | password = secret("SIGNING_PASSWORD")
77 | }
78 |
79 | // use "deployLocal" to deploy to local maven repository
80 | localSpec {
81 | directory.set(rootProject.layout.buildDirectory.get().dir("inspect"))
82 | signing {
83 | key = absent()
84 | password = absent()
85 | }
86 | }
87 |
88 | // use "deployNexus" to deploy to OSSRH / maven central
89 | nexusSpec {
90 | auth.user = secret("SONATYPE_USER")
91 | auth.password = secret("SONATYPE_PASSWORD")
92 | syncToMavenCentral = true
93 | }
94 |
95 | // use "deployNexusSnapshot" to deploy to sonatype snapshots repo
96 | nexusSpec("snapshot") {
97 | auth.user = secret("SONATYPE_USER")
98 | auth.password = secret("SONATYPE_PASSWORD")
99 | repositoryUrl = ossrhSnapshots1
100 | release.version = "latest-SNAPSHOT"
101 | }
102 |
103 | // use "deployGithub" to deploy to github packages
104 | githubSpec {
105 | repository = "Transcoder"
106 | owner = "deepmedia"
107 | auth {
108 | user = secret("GHUB_USER")
109 | token = secret("GHUB_PERSONAL_ACCESS_TOKEN")
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/lib/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/yuya.tanaka/devel/android-sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_102/sample.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_102/sample.mp4
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_137/0.amr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_137/0.amr
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_137/1.amr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_137/1.amr
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_137/2.amr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_137/2.amr
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_137/3.amr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_137/3.amr
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_137/4.amr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_137/4.amr
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_137/5.amr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_137/5.amr
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_137/6.amr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_137/6.amr
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_137/7.amr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_137/7.amr
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_137/8.amr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_137/8.amr
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_137/main.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_137/main.mp3
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_180/party.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_180/party.mp4
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_184/transcode.3gp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_184/transcode.3gp
--------------------------------------------------------------------------------
/lib/src/androidTest/assets/issue_75/bbb_720p_30mb.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepmedia/Transcoder/a28e1eaf575181f2911f857d978e539b41625de0/lib/src/androidTest/assets/issue_75/bbb_720p_30mb.mp4
--------------------------------------------------------------------------------
/lib/src/androidTest/java/com/otaliastudios/transcoder/internal/utils/ISO6709LocationParserTest.java:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.utils;
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4;
4 | import androidx.test.filters.SmallTest;
5 |
6 | import org.junit.Test;
7 | import org.junit.runner.RunWith;
8 |
9 | import static org.junit.Assert.assertArrayEquals;
10 | import static org.junit.Assert.assertNull;
11 |
12 | @RunWith(AndroidJUnit4.class)
13 | @SmallTest
14 | public class ISO6709LocationParserTest {
15 |
16 | @Test
17 | public void testParse() {
18 | ISO6709LocationParser parser = new ISO6709LocationParser();
19 | assertArrayEquals(new float[]{35.658632f, 139.745411f}, parser.parse("+35.658632+139.745411/"), 0);
20 | assertArrayEquals(new float[]{40.75f, -074.00f}, parser.parse("+40.75-074.00/"), 0);
21 | // with Altitude
22 | assertArrayEquals(new float[]{-90f, +0f}, parser.parse("-90+000+2800/"), 0);
23 | assertArrayEquals(new float[]{27.5916f, 086.5640f}, parser.parse("+27.5916+086.5640+8850/"), 0);
24 | // ranged data
25 | assertArrayEquals(new float[]{35.331f, 134.224f}, parser.parse("+35.331+134.224/+35.336+134.228/"), 0);
26 | assertArrayEquals(new float[]{35.331f, 134.224f}, parser.parse("+35.331+134.224/+35.336+134.228/+35.333+134.229/+35.333+134.227/"), 0);
27 | }
28 |
29 | @Test
30 | public void testParseFailure() {
31 | ISO6709LocationParser parser = new ISO6709LocationParser();
32 | assertNull(parser.parse(null));
33 | assertNull(parser.parse(""));
34 | assertNull(parser.parse("35 deg 65' 86.32\" N, 139 deg 74' 54.11\" E"));
35 | assertNull(parser.parse("+35.658632"));
36 | assertNull(parser.parse("+35.658632-"));
37 | assertNull(parser.parse("40.75-074.00"));
38 | assertNull(parser.parse("+40.75-074.00.00"));
39 | }
40 | }
--------------------------------------------------------------------------------
/lib/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/Thumbnailer.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder
2 |
3 | import com.otaliastudios.transcoder.internal.thumbnails.ThumbnailsEngine
4 | import com.otaliastudios.transcoder.internal.utils.ThreadPool
5 | import com.otaliastudios.transcoder.thumbnail.Thumbnail
6 | import java.util.concurrent.Callable
7 | import java.util.concurrent.Future
8 |
9 | class Thumbnailer private constructor() {
10 |
11 | fun thumbnails(options: ThumbnailerOptions): Future {
12 | return ThreadPool.executor.submit(Callable {
13 | ThumbnailsEngine.thumbnails(options)
14 | null
15 | })
16 | }
17 |
18 | fun thumbnails(builder: ThumbnailerOptions.Builder.() -> Unit) = thumbnails(
19 | options = ThumbnailerOptions.Builder().apply(builder).build()
20 | )
21 |
22 | companion object {
23 | // Just for consistency with Transcoder class.
24 | fun getInstance() = Thumbnailer()
25 | }
26 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/ThumbnailerListener.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder
2 |
3 | import com.otaliastudios.transcoder.thumbnail.Thumbnail
4 |
5 | interface ThumbnailerListener {
6 |
7 | fun onThumbnail(thumbnail: Thumbnail)
8 |
9 | fun onThumbnailsCompleted(thumbnails: List) = Unit
10 |
11 | fun onThumbnailsCanceled() = Unit
12 |
13 | fun onThumbnailsFailed(exception: Throwable)
14 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 Yuya Tanaka
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.otaliastudios.transcoder;
17 |
18 | import android.os.Build;
19 |
20 | import com.otaliastudios.transcoder.internal.transcode.TranscodeEngine;
21 | import com.otaliastudios.transcoder.internal.utils.ThreadPool;
22 | import com.otaliastudios.transcoder.sink.DataSink;
23 | import com.otaliastudios.transcoder.validator.Validator;
24 |
25 | import java.io.FileDescriptor;
26 | import java.util.concurrent.Callable;
27 | import java.util.concurrent.Future;
28 |
29 | import androidx.annotation.NonNull;
30 | import androidx.annotation.RequiresApi;
31 |
32 | public class Transcoder {
33 | /**
34 | * Constant for {@link TranscoderListener#onTranscodeCompleted(int)}.
35 | * Transcoding was executed successfully.
36 | */
37 | public static final int SUCCESS_TRANSCODED = 0;
38 |
39 | /**
40 | * Constant for {@link TranscoderListener#onTranscodeCompleted(int)}:
41 | * transcoding was not executed because it was considered
42 | * not necessary by the {@link Validator}.
43 | */
44 | public static final int SUCCESS_NOT_NEEDED = 1;
45 |
46 | @SuppressWarnings("WeakerAccess")
47 | @NonNull
48 | public static Transcoder getInstance() {
49 | return new Transcoder();
50 | }
51 |
52 | private Transcoder() { /* private */ }
53 |
54 | /**
55 | * Starts building transcoder options.
56 | * Requires a non null absolute path to the output file.
57 | *
58 | * @param outPath path to output file
59 | * @return an options builder
60 | */
61 | @NonNull
62 | public static TranscoderOptions.Builder into(@NonNull String outPath) {
63 | return new TranscoderOptions.Builder(outPath);
64 | }
65 |
66 | /**
67 | * Starts building transcoder options.
68 | * Requires a non null fileDescriptor to the output file or stream
69 | *
70 | * @param fileDescriptor descriptor of the output file or stream
71 | * @return an options builder
72 | */
73 | @RequiresApi(api = Build.VERSION_CODES.O)
74 | @NonNull
75 | public static TranscoderOptions.Builder into(@NonNull FileDescriptor fileDescriptor) {
76 | return new TranscoderOptions.Builder(fileDescriptor);
77 | }
78 |
79 | /**
80 | * Starts building transcoder options.
81 | * Requires a non null sink.
82 | *
83 | * @param dataSink the output sink
84 | * @return an options builder
85 | */
86 | @NonNull
87 | public static TranscoderOptions.Builder into(@NonNull DataSink dataSink) {
88 | return new TranscoderOptions.Builder(dataSink);
89 | }
90 |
91 | /**
92 | * Transcodes video file asynchronously.
93 | *
94 | * @param options The transcoder options.
95 | * @return a Future that completes when transcoding is completed
96 | */
97 | @NonNull
98 | public Future transcode(@NonNull final TranscoderOptions options) {
99 | return ThreadPool.getExecutor().submit(new Callable() {
100 | @Override
101 | public Void call() throws Exception {
102 | TranscodeEngine.transcode(options);
103 | return null;
104 | }
105 | });
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/TranscoderListener.java:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder;
2 |
3 | import android.os.Handler;
4 |
5 | import androidx.annotation.NonNull;
6 |
7 | /**
8 | * Listeners for transcoder events. All the callbacks are called on the handler
9 | * specified with {@link TranscoderOptions.Builder#setListenerHandler(Handler)}.
10 | */
11 | public interface TranscoderListener {
12 | /**
13 | * Called to notify progress.
14 | *
15 | * @param progress Progress in [0.0, 1.0] range, or negative value if progress is unknown.
16 | */
17 | void onTranscodeProgress(double progress);
18 |
19 | /**
20 | * Called when transcode completed. The success code can be either
21 | * {@link Transcoder#SUCCESS_TRANSCODED} or {@link Transcoder#SUCCESS_NOT_NEEDED}.
22 | *
23 | * @param successCode the success code
24 | */
25 | void onTranscodeCompleted(int successCode);
26 |
27 | /**
28 | * Called when transcode canceled.
29 | */
30 | void onTranscodeCanceled();
31 |
32 | /**
33 | * Called when transcode failed.
34 | * @param exception the failure exception
35 | */
36 | void onTranscodeFailed(@NonNull Throwable exception);
37 | }
38 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/common/ExactSize.java:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.common;
2 |
3 | import com.otaliastudios.transcoder.resize.Resizer;
4 |
5 | /**
6 | * A special {@link Size} that knows about which dimension is width
7 | * and which is height.
8 | *
9 | * See comments in {@link Resizer}.
10 | */
11 | public class ExactSize extends Size {
12 |
13 | private final int mWidth;
14 | private final int mHeight;
15 |
16 | public ExactSize(int width, int height) {
17 | super(width, height);
18 | mWidth = width;
19 | mHeight = height;
20 | }
21 |
22 | public int getWidth() {
23 | return mWidth;
24 | }
25 |
26 | public int getHeight() {
27 | return mHeight;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/common/Size.java:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.common;
2 |
3 | /**
4 | * Represents a video size in pixels,
5 | * with no notion of rotation / width / height.
6 | * This is just a minor dim and a major dim.
7 | */
8 | public class Size {
9 |
10 | private final int mMajor;
11 | private final int mMinor;
12 |
13 | /**
14 | * The order does not matter.
15 | * @param firstSize one dimension
16 | * @param secondSize the other
17 | */
18 | @SuppressWarnings("WeakerAccess")
19 | public Size(int firstSize, int secondSize) {
20 | mMajor = Math.max(firstSize, secondSize);
21 | mMinor = Math.min(firstSize, secondSize);
22 | }
23 |
24 | public int getMinor() {
25 | return mMinor;
26 | }
27 |
28 | public int getMajor() {
29 | return mMajor;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/common/TrackStatus.java:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.common;
2 |
3 | /**
4 | * Represents the status of a given track inside the transcoding operation.
5 | */
6 | public enum TrackStatus {
7 | /**
8 | * Track was absent in the source.
9 | */
10 | ABSENT,
11 |
12 | /**
13 | * We are removing the track in the output.
14 | */
15 | REMOVING,
16 |
17 | /**
18 | * We are not touching the track.
19 | */
20 | PASS_THROUGH,
21 |
22 | /**
23 | * We are compressing the track in the output.
24 | */
25 | COMPRESSING;
26 |
27 | /**
28 | * This is used to understand whether we should select this track
29 | * in MediaExtractor, and add this track to MediaMuxer.
30 | * Basically if it should be read and written or not
31 | * (no point in just reading without writing).
32 | *
33 | * @return true if transcoding
34 | */
35 | public boolean isTranscoding() {
36 | switch (this) {
37 | case PASS_THROUGH:
38 | case COMPRESSING:
39 | return true;
40 | case REMOVING:
41 | case ABSENT:
42 | return false;
43 | }
44 | throw new RuntimeException("Unexpected track status: " + this);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/common/TrackType.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.common
2 |
3 | import android.media.MediaFormat
4 |
5 | enum class TrackType(internal val displayName: String) {
6 | AUDIO("Audio"), VIDEO("Video");
7 |
8 | }
9 |
10 | internal val MediaFormat.trackType get() = requireNotNull(trackTypeOrNull) {
11 | "Unexpected mime type: ${getString(MediaFormat.KEY_MIME)}"
12 | }
13 |
14 | internal val MediaFormat.trackTypeOrNull get() = when {
15 | getString(MediaFormat.KEY_MIME)!!.startsWith("audio/") -> TrackType.AUDIO
16 | getString(MediaFormat.KEY_MIME)!!.startsWith("video/") -> TrackType.VIDEO
17 | else -> null
18 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/DataSources.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal
2 |
3 | import com.otaliastudios.transcoder.ThumbnailerOptions
4 | import com.otaliastudios.transcoder.TranscoderOptions
5 | import com.otaliastudios.transcoder.common.TrackType
6 | import com.otaliastudios.transcoder.internal.utils.Logger
7 | import com.otaliastudios.transcoder.internal.utils.TrackMap
8 | import com.otaliastudios.transcoder.internal.utils.trackMapOf
9 | import com.otaliastudios.transcoder.source.BlankAudioDataSource
10 | import com.otaliastudios.transcoder.source.DataSource
11 |
12 | internal class DataSources private constructor(
13 | videoSources: List,
14 | audioSources: List,
15 | ) : TrackMap> {
16 |
17 | constructor(options: TranscoderOptions) : this(options.videoDataSources, options.audioDataSources)
18 | constructor(options: ThumbnailerOptions) : this(options.dataSources, listOf())
19 |
20 | private val log = Logger("DataSources")
21 |
22 | private fun DataSource.init() = if (!isInitialized) initialize() else Unit
23 | private fun DataSource.deinit() = if (isInitialized) deinitialize() else Unit
24 | private fun List.init() = forEach {
25 | log.i("initializing $it... (isInit=${it.isInitialized})")
26 | it.init()
27 | }
28 | private fun List.deinit() = forEach {
29 | log.i("deinitializing $it... (isInit=${it.isInitialized})")
30 | it.deinit()
31 | }
32 |
33 | init {
34 | log.i("initializing videoSources...")
35 | videoSources.init()
36 | log.i("initializing audioSources...")
37 | audioSources.init()
38 | }
39 |
40 | // Save and deinit on release, because a source that is discarded for video
41 | // might be active for audio. We don't want to deinit right away.
42 | private val discarded = mutableListOf()
43 |
44 | private val videoSources: List = run {
45 | val valid = videoSources.count { it.getTrackFormat(TrackType.VIDEO) != null }
46 | when (valid) {
47 | 0 -> listOf().also { discarded += videoSources }
48 | videoSources.size -> videoSources
49 | else -> videoSources // Tracks will crash
50 | }
51 | }
52 |
53 | private val audioSources: List = run {
54 | val valid = audioSources.count { it.getTrackFormat(TrackType.AUDIO) != null }
55 | log.i("computing audioSources, valid=$valid")
56 | when (valid) {
57 | 0 -> listOf().also { discarded += audioSources }
58 | audioSources.size -> audioSources
59 | else -> {
60 | // Some tracks do not have audio, while some do. Replace with BlankAudio.
61 | audioSources.map { source ->
62 | if (source.getTrackFormat(TrackType.AUDIO) != null) source
63 | else BlankAudioDataSource(source.durationUs).also {
64 | discarded += source
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
71 | override fun get(type: TrackType) = when (type) {
72 | TrackType.AUDIO -> audioSources
73 | TrackType.VIDEO -> videoSources
74 | }
75 |
76 | override fun has(type: TrackType) = this[type].isNotEmpty()
77 |
78 | fun all() = (audio + video).distinct()
79 |
80 | fun release() {
81 | log.i("release(): releasing...")
82 | video.deinit()
83 | audio.deinit()
84 | discarded.deinit()
85 | log.i("release(): released.")
86 | }
87 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/Segment.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal
2 |
3 | import com.otaliastudios.transcoder.common.TrackType
4 | import com.otaliastudios.transcoder.internal.pipeline.Pipeline
5 | import com.otaliastudios.transcoder.internal.pipeline.State
6 |
7 | internal class Segment(
8 | val type: TrackType,
9 | val index: Int,
10 | private val pipeline: Pipeline,
11 | ) {
12 |
13 | // private val log = Logger("Segment($type,$index)")
14 | private var state: State? = null
15 |
16 | fun advance(): Boolean {
17 | state = pipeline.execute()
18 | return state is State.Ok
19 | }
20 |
21 | fun canAdvance(): Boolean {
22 | // log.v("canAdvance(): state=$state")
23 | return state == null || state !is State.Eos
24 | }
25 |
26 | fun needsSleep(): Boolean {
27 | when(val s = state ?: return false) {
28 | is State.Ok -> return false
29 | is State.Failure -> return s.sleep
30 | }
31 | }
32 |
33 | fun release() {
34 | pipeline.release()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/Tracks.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal
2 |
3 | import android.media.MediaFormat
4 | import com.otaliastudios.transcoder.common.TrackStatus
5 | import com.otaliastudios.transcoder.common.TrackType
6 | import com.otaliastudios.transcoder.internal.media.MediaFormatProvider
7 | import com.otaliastudios.transcoder.internal.utils.Logger
8 | import com.otaliastudios.transcoder.internal.utils.TrackMap
9 | import com.otaliastudios.transcoder.internal.utils.trackMapOf
10 | import com.otaliastudios.transcoder.source.DataSource
11 | import com.otaliastudios.transcoder.strategy.TrackStrategy
12 |
13 | internal class Tracks(
14 | strategies: TrackMap,
15 | sources: DataSources,
16 | videoRotation: Int,
17 | forceCompression: Boolean
18 | ) {
19 |
20 | private val log = Logger("Tracks")
21 |
22 | val all: TrackMap
23 |
24 | val outputFormats: TrackMap
25 |
26 | init {
27 | val (audioFormat, audioStatus) = resolveTrack(TrackType.AUDIO, strategies.audio, sources.audioOrNull())
28 | val (videoFormat, videoStatus) = resolveTrack(TrackType.VIDEO, strategies.video, sources.videoOrNull())
29 | all = trackMapOf(
30 | video = resolveVideoStatus(videoStatus, forceCompression, videoRotation),
31 | audio = resolveAudioStatus(audioStatus, forceCompression)
32 | )
33 | outputFormats = trackMapOf(video = videoFormat, audio = audioFormat)
34 | log.i("init: videoStatus=$videoStatus, resolvedVideoStatus=${all.video}, videoFormat=$videoFormat")
35 | log.i("init: audioStatus=$audioStatus, resolvedAudioStatus=${all.audio}, audioFormat=$audioFormat")
36 | }
37 |
38 | val active: TrackMap = trackMapOf(
39 | video = all.video.takeIf { it.isTranscoding },
40 | audio = all.audio.takeIf { it.isTranscoding }
41 | )
42 |
43 | private fun resolveVideoStatus(status: TrackStatus, forceCompression: Boolean, rotation: Int): TrackStatus {
44 | val force = forceCompression || rotation != 0
45 | val canForce = status == TrackStatus.PASS_THROUGH
46 | return if (canForce && force) TrackStatus.COMPRESSING else status
47 | }
48 |
49 | private fun resolveAudioStatus(status: TrackStatus, forceCompression: Boolean): TrackStatus {
50 | val force = forceCompression
51 | val canForce = status == TrackStatus.PASS_THROUGH
52 | return if (canForce && force) TrackStatus.COMPRESSING else status
53 | }
54 |
55 | private fun resolveTrack(
56 | type: TrackType,
57 | strategy: TrackStrategy,
58 | sources: List? // null or not-empty
59 | ): Pair {
60 | log.i("resolveTrack($type), sources=${sources?.size}, strategy=${strategy::class.simpleName}")
61 | if (sources == null) {
62 | return MediaFormat() to TrackStatus.ABSENT
63 | }
64 |
65 | val provider = MediaFormatProvider()
66 | val inputs = sources.mapNotNull {
67 | val format = it.getTrackFormat(type) ?: return@mapNotNull null
68 | provider.provideMediaFormat(it, type, format)
69 | }
70 |
71 | // The DataSources class already tries to address this for audio, by inserting
72 | // a BlankAudioDataSource. However we still don't have a solution for video.
73 | return when (inputs.size) {
74 | 0 -> MediaFormat() to TrackStatus.ABSENT
75 | sources.size -> {
76 | val output = MediaFormat()
77 | val status = strategy.createOutputFormat(inputs, output)
78 | output to status
79 | }
80 | else -> error("Of all $type sources, some have a $type track, some don't.")
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/conversions.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.audio
2 |
3 | import kotlin.math.ceil
4 |
5 | private const val BYTES_PER_SAMPLE_PER_CHANNEL = 2 // Assuming 16bit audio, so 2
6 | private const val MICROSECONDS_PER_SECOND = 1000000L
7 |
8 | internal fun bytesToUs(
9 | bytes: Int /* bytes */,
10 | sampleRate: Int /* samples/sec */,
11 | channels: Int /* channel */
12 | ): Long {
13 | val byteRatePerChannel = sampleRate * BYTES_PER_SAMPLE_PER_CHANNEL // bytes/sec/channel
14 | val byteRate = byteRatePerChannel * channels // bytes/sec
15 | return MICROSECONDS_PER_SECOND * bytes / byteRate // usec
16 | }
17 |
18 | internal fun bitRate(sampleRate: Int, channels: Int): Int {
19 | val byteRate = channels * sampleRate * BYTES_PER_SAMPLE_PER_CHANNEL
20 | return byteRate * 8
21 | }
22 |
23 | internal fun samplesToBytes(samples: Int, channels: Int): Int {
24 | val bytesPerSample = BYTES_PER_SAMPLE_PER_CHANNEL * channels
25 | return samples * bytesPerSample
26 | }
27 |
28 | internal fun usToBytes(us: Long, sampleRate: Int, channels: Int): Int {
29 | val byteRatePerChannel = sampleRate * BYTES_PER_SAMPLE_PER_CHANNEL
30 | val byteRate = byteRatePerChannel * channels
31 | return ceil(us.toDouble() * byteRate / MICROSECONDS_PER_SECOND).toInt()
32 | }
33 |
34 | internal fun shortsToUs(shorts: Int, sampleRate: Int, channels: Int): Long {
35 | return bytesToUs(shorts * BYTES_PER_SHORT, sampleRate, channels)
36 | }
37 |
38 | internal fun usToShorts(us: Long, sampleRate: Int, channels: Int): Int {
39 | return usToBytes(us, sampleRate, channels) / BYTES_PER_SHORT
40 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/remix/AudioRemixer.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.audio.remix
2 |
3 | import java.nio.ShortBuffer
4 |
5 | /**
6 | * Remixes audio data. See [DownMixAudioRemixer], [UpMixAudioRemixer] or [PassThroughAudioRemixer]
7 | * for concrete implementations.
8 | */
9 | internal interface AudioRemixer {
10 |
11 | /**
12 | * Remixes input audio from input buffer into the output buffer.
13 | * The output buffer is guaranteed to have a [ShortBuffer.remaining] size that is
14 | * consistent with [getRemixedSize].
15 | */
16 | fun remix(inputBuffer: ShortBuffer, outputBuffer: ShortBuffer)
17 |
18 | /**
19 | * Returns the output size (in shorts) needed to process an input buffer of the
20 | * given [inputSize] (in shorts).
21 | */
22 | fun getRemixedSize(inputSize: Int): Int
23 |
24 | companion object {
25 | internal operator fun get(inputChannels: Int, outputChannels: Int): AudioRemixer = when {
26 | inputChannels == outputChannels -> PassThroughAudioRemixer()
27 | inputChannels !in setOf(1, 2) -> error("Input channel count not supported: $inputChannels")
28 | outputChannels !in setOf(1, 2) -> error("Output channel count not supported: $outputChannels")
29 | inputChannels < outputChannels -> UpMixAudioRemixer()
30 | else -> DownMixAudioRemixer()
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/remix/DownMixAudioRemixer.java:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.audio.remix;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.otaliastudios.transcoder.internal.audio.remix.AudioRemixer;
6 |
7 | import java.nio.ShortBuffer;
8 |
9 | /**
10 | * A {@link AudioRemixer} that downmixes stereo audio to mono.
11 | */
12 | public class DownMixAudioRemixer implements AudioRemixer {
13 |
14 | private static final int SIGNED_SHORT_LIMIT = 32768;
15 | private static final int UNSIGNED_SHORT_MAX = 65535;
16 |
17 | @Override
18 | public void remix(@NonNull final ShortBuffer inputBuffer, @NonNull final ShortBuffer outputBuffer) {
19 | // Down-mix stereo to mono
20 | // Viktor Toth's algorithm -
21 | // See: http://www.vttoth.com/CMS/index.php/technical-notes/68
22 | // http://stackoverflow.com/a/25102339
23 | final int inRemaining = inputBuffer.remaining() / 2;
24 | final int outSpace = outputBuffer.remaining();
25 |
26 | final int samplesToBeProcessed = Math.min(inRemaining, outSpace);
27 | for (int i = 0; i < samplesToBeProcessed; ++i) {
28 | // Convert to unsigned
29 | final int a = inputBuffer.get() + SIGNED_SHORT_LIMIT;
30 | final int b = inputBuffer.get() + SIGNED_SHORT_LIMIT;
31 | int m;
32 | // Pick the equation
33 | if ((a < SIGNED_SHORT_LIMIT) || (b < SIGNED_SHORT_LIMIT)) {
34 | // Viktor's first equation when both sources are "quiet"
35 | // (i.e. less than middle of the dynamic range)
36 | m = a * b / SIGNED_SHORT_LIMIT;
37 | } else {
38 | // Viktor's second equation when one or both sources are loud
39 | m = 2 * (a + b) - (a * b) / SIGNED_SHORT_LIMIT - UNSIGNED_SHORT_MAX;
40 | }
41 | // Convert output back to signed short
42 | if (m == UNSIGNED_SHORT_MAX + 1) m = UNSIGNED_SHORT_MAX;
43 | outputBuffer.put((short) (m - SIGNED_SHORT_LIMIT));
44 | }
45 | }
46 |
47 | @Override
48 | public int getRemixedSize(int inputSize) {
49 | return inputSize / 2;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/remix/PassThroughAudioRemixer.java:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.audio.remix;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.otaliastudios.transcoder.internal.audio.remix.AudioRemixer;
6 |
7 | import java.nio.ShortBuffer;
8 |
9 | /**
10 | * The simplest {@link AudioRemixer} that does nothing.
11 | */
12 | public class PassThroughAudioRemixer implements AudioRemixer {
13 |
14 | @Override
15 | public void remix(@NonNull final ShortBuffer inputBuffer, @NonNull final ShortBuffer outputBuffer) {
16 | outputBuffer.put(inputBuffer);
17 | }
18 |
19 | @Override
20 | public int getRemixedSize(int inputSize) {
21 | return inputSize;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/remix/UpMixAudioRemixer.java:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.audio.remix;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.otaliastudios.transcoder.internal.audio.remix.AudioRemixer;
6 |
7 | import java.nio.ShortBuffer;
8 |
9 | /**
10 | * A {@link AudioRemixer} that upmixes mono audio to stereo.
11 | */
12 | public class UpMixAudioRemixer implements AudioRemixer {
13 |
14 | @Override
15 | public void remix(@NonNull final ShortBuffer inputBuffer, @NonNull final ShortBuffer outputBuffer) {
16 | // Up-mix mono to stereo
17 | final int inRemaining = inputBuffer.remaining();
18 | final int outSpace = outputBuffer.remaining() / 2;
19 |
20 | final int samplesToBeProcessed = Math.min(inRemaining, outSpace);
21 | for (int i = 0; i < samplesToBeProcessed; ++i) {
22 | final short inSample = inputBuffer.get();
23 | outputBuffer.put(inSample);
24 | outputBuffer.put(inSample);
25 | }
26 | }
27 |
28 | @Override
29 | public int getRemixedSize(int inputSize) {
30 | return inputSize * 2;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/audio/shorts.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.audio
2 |
3 | import java.nio.ByteBuffer
4 | import java.nio.ByteOrder
5 | import java.nio.ShortBuffer
6 |
7 | internal const val BYTES_PER_SHORT = 2
8 |
9 | internal class ShortBuffers {
10 | private val map = mutableMapOf()
11 |
12 | fun acquire(name: String, size: Int): ShortBuffer {
13 | var current = map[name]
14 | if (current == null || current.capacity() < size) {
15 | current = ByteBuffer.allocateDirect(size * BYTES_PER_SHORT)
16 | .order(ByteOrder.nativeOrder())
17 | .asShortBuffer()
18 | }
19 | current!!.clear()
20 | current.limit(size)
21 | return current.also {
22 | map[name] = current
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderDropper.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.codec
2 |
3 | import com.otaliastudios.transcoder.internal.utils.Logger
4 | import com.otaliastudios.transcoder.source.DataSource
5 |
6 | /**
7 | * Hard to read, late-night class that takes decoder input frames along with their render
8 | * boolean coming from [DataSource.Chunk.render] and understands which periods of time should be
9 | * rendered and which shouldn't.
10 | *
11 | * These ranges are then matched in the decoder raw output frames. Those that do not belong to
12 | * a render period are dropped, the others are rendered but their timestamp is shifted according
13 | * to the no-render periods behind. So we do two things:
14 | *
15 | * 1. don't render [output] frames belonging to [input] frames that had render = false
16 | * 2. adjust [output] timestamps to account for the no-render periods
17 | *
18 | * The second feature can be disabled by setting the [continuous] boolean to false.
19 | *
20 | * NOTE: we assumes that the [input] timestamps are monotonic. If they are not, everything
21 | * is screwed. Also, if the source jumps forward using seek, we won't catch the jump. This class
22 | * catches discontinuities only through changes in the render boolean passed to [input].
23 | */
24 | internal class DecoderDropper(private val continuous: Boolean) {
25 |
26 | private val log = Logger("DecoderDropper")
27 | private val closedDeltas = mutableMapOf()
28 | private val closedRanges = mutableListOf()
29 | private var pendingRange: LongRange? = null
30 |
31 | private var firstInputUs: Long? = null
32 | private var firstOutputUs: Long? = null
33 |
34 | private fun debug(message: String, important: Boolean = false) {
35 | /* val full = "$message firstInputUs=$firstInputUs " +
36 | "validInputUs=[${closedRanges.joinToString {
37 | "$it(deltaUs=${closedDeltas[it]})"
38 | }}] pendingRangeUs=${pendingRange}"
39 | if (important) log.w(full) else log.v(full) */
40 | }
41 |
42 | fun input(timeUs: Long, render: Boolean) {
43 | if (firstInputUs == null) {
44 | firstInputUs = timeUs
45 | }
46 | if (render) {
47 | debug("INPUT: inputUs=$timeUs")
48 | // log.v("TDBG inputUs=$timeUs")
49 | if (pendingRange == null) pendingRange = timeUs..Long.MAX_VALUE
50 | else pendingRange = pendingRange!!.first..timeUs
51 | } else {
52 | debug("INPUT: Got SKIPPING input! inputUs=$timeUs")
53 | if (pendingRange != null && pendingRange!!.last != Long.MAX_VALUE) {
54 | closedRanges.add(pendingRange!!)
55 | closedDeltas[pendingRange!!] = if (closedRanges.size >= 2) {
56 | pendingRange!!.first - closedRanges[closedRanges.lastIndex - 1].last
57 | } else 0L
58 | }
59 | pendingRange = null
60 | }
61 | }
62 |
63 | fun output(timeUs: Long): Long? {
64 | if (firstOutputUs == null) {
65 | firstOutputUs = timeUs
66 | }
67 | val timeInInputScaleUs = firstInputUs!! + (timeUs - firstOutputUs!!)
68 | var deltaUs = 0L
69 | closedRanges.forEach {
70 | deltaUs += closedDeltas[it]!!
71 | if (it.contains(timeInInputScaleUs)) {
72 | debug("OUTPUT: Rendering! outputTimeUs=$timeUs newOutputTimeUs=${timeUs - deltaUs} deltaUs=$deltaUs")
73 | // log.v("TDBG outputUs=$timeUs")
74 | return if (continuous) timeUs - deltaUs
75 | else timeUs
76 | }
77 | }
78 | if (pendingRange != null) {
79 | if (pendingRange!!.contains(timeInInputScaleUs)) {
80 | if (closedRanges.isNotEmpty()) {
81 | deltaUs += pendingRange!!.first - closedRanges.last().last
82 | }
83 | debug("OUTPUT: Rendering! outputTimeUs=$timeUs newOutputTimeUs=${timeUs - deltaUs} deltaUs=$deltaUs")
84 | // log.v("TDBG outputUs=$timeUs")
85 | return if (continuous) timeUs - deltaUs
86 | else timeUs
87 | }
88 | }
89 | debug("OUTPUT: SKIPPING! outputTimeUs=$timeUs", important = true)
90 | return null
91 | }
92 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/DecoderTimer.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.codec
2 |
3 | import com.otaliastudios.transcoder.common.TrackType
4 | import com.otaliastudios.transcoder.internal.pipeline.TransformStep
5 | import com.otaliastudios.transcoder.internal.pipeline.State
6 | import com.otaliastudios.transcoder.time.TimeInterpolator
7 | import java.nio.ByteBuffer
8 |
9 | internal class DecoderTimerData(
10 | buffer: ByteBuffer,
11 | val rawTimeUs: Long,
12 | timeUs: Long,
13 | val timeStretch: Double,
14 | release: (render: Boolean) -> Unit
15 | ) : DecoderData(buffer, timeUs, release)
16 |
17 | internal class DecoderTimer(
18 | private val track: TrackType,
19 | private val interpolator: TimeInterpolator,
20 | ) : TransformStep("DecoderTimer") {
21 |
22 | private var lastTimeUs: Long = Long.MIN_VALUE
23 | private var lastRawTimeUs: Long = Long.MIN_VALUE
24 |
25 | override fun advance(state: State.Ok): State {
26 | if (state is State.Eos) return state
27 | require(state.value !is DecoderTimerData) {
28 | "Can't apply DecoderTimer twice."
29 | }
30 | val rawTimeUs = state.value.timeUs
31 | val timeUs = interpolator.interpolate(track, rawTimeUs)
32 | val timeStretch = if (lastTimeUs == Long.MIN_VALUE) {
33 | 1.0
34 | } else {
35 | // TODO to be exact, timeStretch should be computed by comparing the NEXT timestamps
36 | // with this, instead of comparing this with the PREVIOUS
37 | val durationUs = timeUs - lastTimeUs
38 | val rawDurationUs = rawTimeUs - lastRawTimeUs
39 | durationUs.toDouble() / rawDurationUs
40 | }
41 | lastTimeUs = timeUs
42 | lastRawTimeUs = rawTimeUs
43 |
44 | return State.Ok(DecoderTimerData(
45 | buffer = state.value.buffer,
46 | rawTimeUs = rawTimeUs,
47 | timeUs = timeUs,
48 | timeStretch = timeStretch,
49 | release = state.value.release
50 | ))
51 | }
52 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Bridge.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.data
2 |
3 | import android.media.MediaCodec
4 | import android.media.MediaFormat
5 | import com.otaliastudios.transcoder.internal.pipeline.BaseStep
6 | import com.otaliastudios.transcoder.internal.pipeline.State
7 | import com.otaliastudios.transcoder.internal.pipeline.Step
8 | import com.otaliastudios.transcoder.internal.utils.Logger
9 | import java.nio.ByteBuffer
10 | import java.nio.ByteOrder
11 |
12 | internal class Bridge(private val format: MediaFormat)
13 | : BaseStep("Bridge"), ReaderChannel {
14 |
15 | private val bufferSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
16 | private val buffer = ByteBuffer.allocateDirect(bufferSize).order(ByteOrder.nativeOrder())
17 | override val channel = this
18 |
19 | override fun buffer(): Pair {
20 | buffer.clear()
21 | return buffer to 0
22 | }
23 |
24 | override fun initialize(next: WriterChannel) {
25 | log.i("initialize(): format=$format")
26 | next.handleFormat(format)
27 | }
28 |
29 | // Can't do much about chunk.render, since we don't even decode.
30 | override fun advance(state: State.Ok): State {
31 | val (chunk, _) = state.value
32 | val flags = if (chunk.keyframe) MediaCodec.BUFFER_FLAG_SYNC_FRAME else 0
33 | val result = WriterData(chunk.buffer, chunk.timeUs, flags) {}
34 | return if (state is State.Eos) State.Eos(result) else State.Ok(result)
35 | }
36 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Reader.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.data
2 |
3 | import com.otaliastudios.transcoder.common.TrackType
4 | import com.otaliastudios.transcoder.internal.pipeline.BaseStep
5 | import com.otaliastudios.transcoder.internal.pipeline.Channel
6 | import com.otaliastudios.transcoder.internal.pipeline.State
7 | import com.otaliastudios.transcoder.source.DataSource
8 | import java.nio.ByteBuffer
9 |
10 |
11 | internal data class ReaderData(val chunk: DataSource.Chunk, val id: Int)
12 |
13 | internal interface ReaderChannel : Channel {
14 | fun buffer(): Pair?
15 | }
16 |
17 | internal class Reader(
18 | private val source: DataSource,
19 | private val track: TrackType
20 | ) : BaseStep("Reader") {
21 |
22 | override val channel = Channel
23 | private val chunk = DataSource.Chunk()
24 |
25 | private inline fun nextBufferOrWait(action: (ByteBuffer, Int) -> State): State {
26 | val buffer = next.buffer()
27 | if (buffer == null) {
28 | // dequeueInputBuffer failed
29 | log.v("Returning State.Wait because buffer is null.")
30 | return State.Retry(true)
31 | } else {
32 | return action(buffer.first, buffer.second)
33 | }
34 | }
35 |
36 | override fun advance(state: State.Ok): State {
37 | return if (source.isDrained) {
38 | log.i("Source is drained! Returning Eos as soon as possible.")
39 | nextBufferOrWait { byteBuffer, id ->
40 | byteBuffer.limit(0)
41 | chunk.buffer = byteBuffer
42 | chunk.keyframe = false
43 | chunk.render = true
44 | State.Eos(ReaderData(chunk, id))
45 | }
46 | } else if (!source.canReadTrack(track)) {
47 | log.i("Returning State.Wait because source can't read $track right now.")
48 | State.Retry(false)
49 | } else {
50 | nextBufferOrWait { byteBuffer, id ->
51 | chunk.buffer = byteBuffer
52 | source.readTrack(chunk)
53 | // log.v("Returning ${chunk.buffer?.remaining() ?: -1} bytes from source")
54 | State.Ok(ReaderData(chunk, id))
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/data/ReaderTimer.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.data
2 |
3 | import com.otaliastudios.transcoder.common.TrackType
4 | import com.otaliastudios.transcoder.internal.pipeline.TransformStep
5 | import com.otaliastudios.transcoder.internal.pipeline.State
6 | import com.otaliastudios.transcoder.time.TimeInterpolator
7 |
8 | internal class ReaderTimer(
9 | private val track: TrackType,
10 | private val interpolator: TimeInterpolator
11 | ) : TransformStep("ReaderTimer") {
12 | override fun advance(state: State.Ok): State {
13 | if (state is State.Eos) return state
14 | state.value.chunk.timeUs = interpolator.interpolate(track, state.value.chunk.timeUs)
15 | return state
16 | }
17 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Seeker.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.data
2 |
3 | import com.otaliastudios.transcoder.common.TrackType
4 | import com.otaliastudios.transcoder.internal.pipeline.BaseStep
5 | import com.otaliastudios.transcoder.internal.pipeline.Channel
6 | import com.otaliastudios.transcoder.internal.pipeline.State
7 | import com.otaliastudios.transcoder.internal.utils.Logger
8 | import com.otaliastudios.transcoder.source.DataSource
9 | import java.nio.ByteBuffer
10 |
11 | internal class Seeker(
12 | private val source: DataSource,
13 | positions: List,
14 | private val seek: (Long) -> Boolean
15 | ) : BaseStep("Seeker") {
16 |
17 | override val channel = Channel
18 | private val positions = positions.toMutableList()
19 |
20 | override fun advance(state: State.Ok): State {
21 | if (positions.isNotEmpty()) {
22 | if (seek(positions.first())) {
23 | log.i("Seeking to next position ${positions.first()}")
24 | val next = positions.removeFirst()
25 | source.seekTo(next)
26 | } else {
27 | // log.v("Not seeking to next Request. head=${positions.first()}")
28 | }
29 | }
30 | return state
31 | }
32 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/data/Writer.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.data
2 |
3 | import android.media.MediaCodec
4 | import android.media.MediaFormat
5 | import com.otaliastudios.transcoder.common.TrackType
6 | import com.otaliastudios.transcoder.internal.pipeline.BaseStep
7 | import com.otaliastudios.transcoder.internal.pipeline.Channel
8 | import com.otaliastudios.transcoder.internal.pipeline.State
9 | import com.otaliastudios.transcoder.internal.pipeline.Step
10 | import com.otaliastudios.transcoder.internal.utils.Logger
11 | import com.otaliastudios.transcoder.sink.DataSink
12 | import java.nio.ByteBuffer
13 |
14 | internal data class WriterData(
15 | val buffer: ByteBuffer,
16 | val timeUs: Long,
17 | val flags: Int,
18 | val release: () -> Unit
19 | )
20 |
21 | internal interface WriterChannel : Channel {
22 | fun handleFormat(format: MediaFormat)
23 | }
24 |
25 | internal class Writer(
26 | private val sink: DataSink,
27 | private val track: TrackType
28 | ) : BaseStep("Writer"), WriterChannel {
29 |
30 | override val channel = this
31 |
32 | private val info = MediaCodec.BufferInfo()
33 |
34 | override fun handleFormat(format: MediaFormat) {
35 | log.i("handleFormat($format)")
36 | sink.setTrackFormat(track, format)
37 | }
38 |
39 | override fun advance(state: State.Ok): State {
40 | val (buffer, timestamp, flags) = state.value
41 | // Note: flags does NOT include BUFFER_FLAG_END_OF_STREAM. That's passed via State.Eos.
42 | val eos = state is State.Eos
43 | if (eos) {
44 | // Note: it may happen that at this point, buffer has some data. but creating an extra writeTrack() call
45 | // can cause some crashes that were not properly debugged, probably related to wrong timestamp.
46 | // I think if we could ensure that timestamp is valid (> previous, > 0) and buffer.hasRemaining(), there should
47 | // be an extra call here. See #159. Reluctant to do so without a repro test.
48 | info.set(0, 0, 0, flags or MediaCodec.BUFFER_FLAG_END_OF_STREAM)
49 | } else {
50 | info.set(
51 | buffer.position(),
52 | buffer.remaining(),
53 | timestamp,
54 | flags
55 | )
56 | }
57 | sink.writeTrack(track, buffer, info)
58 | state.value.release()
59 | return if (eos) State.Eos(Unit) else State.Ok(Unit)
60 | }
61 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/media/MediaFormatConstants.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 Yuya Tanaka
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.otaliastudios.transcoder.internal.media;
17 |
18 | public class MediaFormatConstants {
19 |
20 | // from MediaFormat of API level >= 21, but might be usable in older APIs as native code implementation exists.
21 | // https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/ACodec.cpp#2621
22 | // NOTE: native code enforces baseline profile.
23 | // https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/ACodec.cpp#2638
24 | /** For encoder parameter. Use value of MediaCodecInfo.CodecProfileLevel.AVCProfile* . */
25 | public static final String KEY_PROFILE = "profile";
26 |
27 | // from https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/ACodec.cpp#2623
28 | /** For encoder parameter. Use value of MediaCodecInfo.CodecProfileLevel.AVCLevel* . */
29 | public static final String KEY_LEVEL = "level";
30 |
31 | // from https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/MediaCodec.cpp#2197
32 | /** Included in MediaFormat from {@link android.media.MediaExtractor#getTrackFormat(int)}. Value is {@link java.nio.ByteBuffer}. */
33 | @SuppressWarnings("WeakerAccess")
34 | public static final String KEY_AVC_SPS = "csd-0";
35 |
36 | /** Included in MediaFormat from {@link android.media.MediaExtractor#getTrackFormat(int)}. Value is {@link java.nio.ByteBuffer}. */
37 | public static final String KEY_AVC_PPS = "csd-1";
38 |
39 | /**
40 | * For decoder parameter and included in MediaFormat from {@link android.media.MediaExtractor#getTrackFormat(int)}.
41 | * Decoder rotates specified degrees before rendering video to surface.
42 | * NOTE: Only included in track format of API >= 21.
43 | */
44 | public static final String KEY_ROTATION_DEGREES = "rotation-degrees";
45 |
46 | // Video formats
47 | // from MediaFormat of API level >= 21
48 | public static final String MIMETYPE_VIDEO_AVC = "video/avc";
49 | public static final String MIMETYPE_VIDEO_H263 = "video/3gpp";
50 | public static final String MIMETYPE_VIDEO_VP8 = "video/x-vnd.on2.vp8";
51 |
52 | // Audio formats
53 | // from MediaFormat of API level >= 21
54 | public static final String MIMETYPE_AUDIO_AAC = "audio/mp4a-latm";
55 | public static final String MIMETYPE_AUDIO_RAW = "audio/raw";
56 |
57 | private MediaFormatConstants() { }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/State.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.pipeline
2 |
3 | internal sealed interface State {
4 |
5 | // Running
6 | open class Ok(val value: T) : State {
7 | override fun toString() = "State.Ok($value)"
8 | }
9 |
10 | // Run for the last time
11 | class Eos(value: T) : Ok(value) {
12 | override fun toString() = "State.Eos($value)"
13 | }
14 |
15 | // Failed to produce output, try again later
16 | sealed interface Failure : State {
17 | val sleep: Boolean
18 | }
19 |
20 | class Retry(override val sleep: Boolean) : Failure {
21 | override fun toString() = "State.Retry($sleep)"
22 | }
23 |
24 | class Consume(override val sleep: Boolean = false) : Failure {
25 | override fun toString() = "State.Consume($sleep)"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/Step.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.transcoder.internal.pipeline
2 |
3 | // TODO this could be Any
4 | internal interface Channel {
5 | companion object : Channel
6 | }
7 |
8 | internal interface Step<
9 | Input: Any,
10 | InputChannel: Channel,
11 | Output: Any,
12 | OutputChannel: Channel
13 | > {
14 | val name: String
15 | val channel: InputChannel
16 |
17 | fun initialize(next: OutputChannel) = Unit
18 |
19 | fun advance(state: State.Ok): State