├── .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 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/runConfigurations/deployLocal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 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 | [![Build Status](https://github.com/deepmedia/Transcoder/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/deepmedia/Transcoder/actions) 2 | [![Release](https://img.shields.io/github/release/deepmedia/Transcoder.svg)](https://github.com/deepmedia/Transcoder/releases) 3 | [![Issues](https://img.shields.io/github/issues-raw/deepmedia/Transcoder.svg)](https://github.com/deepmedia/Transcoder/issues) 4 | 5 | ![Project logo](assets/logo-256.png) 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 20 | 21 | fun release() = Unit 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/pipelines.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.pipeline 2 | 3 | import android.media.MediaFormat 4 | import com.otaliastudios.transcoder.common.TrackType 5 | import com.otaliastudios.transcoder.internal.Codecs 6 | import com.otaliastudios.transcoder.internal.audio.AudioEngine 7 | import com.otaliastudios.transcoder.internal.data.* 8 | import com.otaliastudios.transcoder.internal.data.Reader 9 | import com.otaliastudios.transcoder.internal.data.ReaderTimer 10 | import com.otaliastudios.transcoder.internal.data.Writer 11 | import com.otaliastudios.transcoder.internal.codec.Decoder 12 | import com.otaliastudios.transcoder.internal.codec.DecoderTimer 13 | import com.otaliastudios.transcoder.internal.codec.Encoder 14 | import com.otaliastudios.transcoder.internal.video.VideoPublisher 15 | import com.otaliastudios.transcoder.internal.video.VideoRenderer 16 | import com.otaliastudios.transcoder.resample.AudioResampler 17 | import com.otaliastudios.transcoder.sink.DataSink 18 | import com.otaliastudios.transcoder.source.DataSource 19 | import com.otaliastudios.transcoder.stretch.AudioStretcher 20 | import com.otaliastudios.transcoder.time.TimeInterpolator 21 | 22 | internal fun EmptyPipeline() = Pipeline.build("Empty") 23 | 24 | internal fun PassThroughPipeline( 25 | track: TrackType, 26 | source: DataSource, 27 | sink: DataSink, 28 | interpolator: TimeInterpolator 29 | ) = Pipeline.build("PassThrough$track") { 30 | Reader(source, track) + 31 | ReaderTimer(track, interpolator) + 32 | Bridge(source.getTrackFormat(track)!!) + 33 | Writer(sink, track) 34 | } 35 | 36 | internal fun RegularPipeline( 37 | track: TrackType, 38 | debug: String?, 39 | source: DataSource, 40 | sink: DataSink, 41 | interpolator: TimeInterpolator, 42 | format: MediaFormat, 43 | codecs: Codecs, 44 | videoRotation: Int, 45 | audioStretcher: AudioStretcher, 46 | audioResampler: AudioResampler 47 | ) = when (track) { 48 | TrackType.VIDEO -> VideoPipeline(debug, source, sink, interpolator, format, codecs, videoRotation) 49 | TrackType.AUDIO -> AudioPipeline(debug, source, sink, interpolator, format, codecs, audioStretcher, audioResampler) 50 | } 51 | 52 | private fun VideoPipeline( 53 | debug: String?, 54 | source: DataSource, 55 | sink: DataSink, 56 | interpolator: TimeInterpolator, 57 | format: MediaFormat, 58 | codecs: Codecs, 59 | videoRotation: Int 60 | ) = Pipeline.build("Video", debug) { 61 | Reader(source, TrackType.VIDEO) + 62 | Decoder(source.getTrackFormat(TrackType.VIDEO)!!, true) + 63 | DecoderTimer(TrackType.VIDEO, interpolator) + 64 | VideoRenderer(source.orientation, videoRotation, format) + 65 | VideoPublisher() + 66 | Encoder(codecs, TrackType.VIDEO) + 67 | Writer(sink, TrackType.VIDEO) 68 | } 69 | 70 | private fun AudioPipeline( 71 | debug: String?, 72 | source: DataSource, 73 | sink: DataSink, 74 | interpolator: TimeInterpolator, 75 | format: MediaFormat, 76 | codecs: Codecs, 77 | audioStretcher: AudioStretcher, 78 | audioResampler: AudioResampler 79 | ) = Pipeline.build("Audio", debug) { 80 | Reader(source, TrackType.AUDIO) + 81 | Decoder(source.getTrackFormat(TrackType.AUDIO)!!, true) + 82 | DecoderTimer(TrackType.AUDIO, interpolator) + 83 | AudioEngine(audioStretcher, audioResampler, format) + 84 | Encoder(codecs, TrackType.AUDIO) + 85 | Writer(sink, TrackType.AUDIO) 86 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/pipeline/steps.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.pipeline 2 | 3 | import com.otaliastudios.transcoder.internal.utils.Logger 4 | 5 | internal abstract class BaseStep< 6 | Input: Any, 7 | InputChannel: Channel, 8 | Output: Any, 9 | OutputChannel: Channel 10 | >(final override val name: String) : Step { 11 | 12 | protected val log = Logger(name) 13 | 14 | protected lateinit var next: OutputChannel 15 | private set 16 | 17 | override fun initialize(next: OutputChannel) { 18 | this.next = next 19 | } 20 | } 21 | 22 | internal abstract class TransformStep(name: String) : BaseStep(name) { 23 | override lateinit var channel: C 24 | override fun initialize(next: C) { 25 | super.initialize(next) 26 | channel = next 27 | } 28 | } 29 | 30 | internal abstract class QueuedStep< 31 | Input: Any, 32 | InputChannel: Channel, 33 | Output: Any, 34 | OutputChannel: Channel 35 | >(name: String) : BaseStep(name) { 36 | 37 | protected abstract fun enqueue(data: Input) 38 | 39 | protected abstract fun enqueueEos(data: Input) 40 | 41 | protected abstract fun drain(): State 42 | 43 | final override fun advance(state: State.Ok): State { 44 | if (state is State.Eos) enqueueEos(state.value) 45 | else enqueue(state.value) 46 | // Disallow State.Retry because the input was already handled. 47 | return when (val result = drain()) { 48 | is State.Retry -> State.Consume(result.sleep) 49 | else -> result 50 | } 51 | } 52 | 53 | fun tryAdvance(): State { 54 | return drain() 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsDispatcher.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.thumbnails; 2 | 3 | import android.os.Handler; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import com.otaliastudios.transcoder.ThumbnailerListener; 8 | import com.otaliastudios.transcoder.ThumbnailerOptions; 9 | import com.otaliastudios.transcoder.thumbnail.Thumbnail; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /** 15 | * Wraps a ThumbnailerListener and posts events on the given handler. 16 | */ 17 | class ThumbnailsDispatcher { 18 | 19 | private final Handler mHandler; 20 | private final ThumbnailerListener mListener; 21 | private final List mResults = new ArrayList<>(); 22 | 23 | ThumbnailsDispatcher(@NonNull ThumbnailerOptions options) { 24 | mHandler = options.getListenerHandler(); 25 | mListener = options.getListener(); 26 | } 27 | 28 | void dispatchCancel() { 29 | mHandler.post(new Runnable() { 30 | @Override 31 | public void run() { 32 | mListener.onThumbnailsCanceled(); 33 | } 34 | }); 35 | } 36 | 37 | void dispatchCompletion() { 38 | mHandler.post(new Runnable() { 39 | @Override 40 | public void run() { 41 | mListener.onThumbnailsCompleted(mResults); 42 | } 43 | }); 44 | } 45 | 46 | void dispatchFailure(@NonNull final Throwable exception) { 47 | mHandler.post(new Runnable() { 48 | @Override 49 | public void run() { 50 | mListener.onThumbnailsFailed(exception); 51 | } 52 | }); 53 | } 54 | 55 | void dispatchThumbnail(@NonNull final Thumbnail thumbnail) { 56 | mResults.add(thumbnail); 57 | mHandler.post(new Runnable() { 58 | @Override 59 | public void run() { 60 | mListener.onThumbnail(thumbnail); 61 | } 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/ThumbnailsEngine.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.thumbnails 2 | 3 | import com.otaliastudios.transcoder.ThumbnailerOptions 4 | import com.otaliastudios.transcoder.Transcoder 5 | import com.otaliastudios.transcoder.TranscoderOptions 6 | import com.otaliastudios.transcoder.internal.DataSources 7 | import com.otaliastudios.transcoder.internal.utils.Logger 8 | import com.otaliastudios.transcoder.internal.utils.trackMapOf 9 | import com.otaliastudios.transcoder.thumbnail.Thumbnail 10 | 11 | internal abstract class ThumbnailsEngine { 12 | 13 | abstract fun thumbnails(progress: (Thumbnail) -> Unit) 14 | 15 | abstract fun cleanup() 16 | 17 | companion object { 18 | private val log = Logger("ThumbnailsEngine") 19 | 20 | private fun Throwable.isInterrupted(): Boolean { 21 | if (this is InterruptedException) return true 22 | if (this == this.cause) return false 23 | return this.cause?.isInterrupted() ?: false 24 | } 25 | 26 | @JvmStatic 27 | fun thumbnails(options: ThumbnailerOptions) { 28 | log.i("thumbnails(): called...") 29 | var engine: ThumbnailsEngine? = null 30 | val dispatcher = ThumbnailsDispatcher(options) 31 | try { 32 | engine = DefaultThumbnailsEngine( 33 | dataSources = DataSources(options), 34 | rotation = options.rotation, 35 | resizer = options.resizer, 36 | requests = options.thumbnailRequests 37 | ) 38 | engine.thumbnails { 39 | dispatcher.dispatchThumbnail(it) 40 | } 41 | dispatcher.dispatchCompletion() 42 | } catch (e: Exception) { 43 | if (e.isInterrupted()) { 44 | log.i("Transcode canceled.", e) 45 | dispatcher.dispatchCancel() 46 | } else { 47 | log.e("Unexpected error while transcoding.", e) 48 | dispatcher.dispatchFailure(e) 49 | throw e 50 | } 51 | } finally { 52 | engine?.cleanup() 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeDispatcher.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.transcode; 2 | 3 | import android.os.Handler; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import com.otaliastudios.transcoder.TranscoderListener; 8 | import com.otaliastudios.transcoder.TranscoderOptions; 9 | 10 | /** 11 | * Wraps a TranscoderListener and posts events on the given handler. 12 | */ 13 | class TranscodeDispatcher { 14 | 15 | private final Handler mHandler; 16 | private final TranscoderListener mListener; 17 | 18 | TranscodeDispatcher(@NonNull TranscoderOptions options) { 19 | mHandler = options.getListenerHandler(); 20 | mListener = options.getListener(); 21 | } 22 | 23 | void dispatchCancel() { 24 | mHandler.post(new Runnable() { 25 | @Override 26 | public void run() { 27 | mListener.onTranscodeCanceled(); 28 | } 29 | }); 30 | } 31 | 32 | void dispatchSuccess(final int successCode) { 33 | mHandler.post(new Runnable() { 34 | @Override 35 | public void run() { 36 | mListener.onTranscodeCompleted(successCode); 37 | } 38 | }); 39 | } 40 | 41 | void dispatchFailure(@NonNull final Throwable exception) { 42 | mHandler.post(new Runnable() { 43 | @Override 44 | public void run() { 45 | mListener.onTranscodeFailed(exception); 46 | } 47 | }); 48 | } 49 | 50 | void dispatchProgress(final double progress) { 51 | mHandler.post(new Runnable() { 52 | @Override 53 | public void run() { 54 | mListener.onTranscodeProgress(progress); 55 | } 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/transcode/TranscodeEngine.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.transcode 2 | 3 | import com.otaliastudios.transcoder.Transcoder 4 | import com.otaliastudios.transcoder.TranscoderOptions 5 | import com.otaliastudios.transcoder.internal.DataSources 6 | import com.otaliastudios.transcoder.internal.utils.Logger 7 | import com.otaliastudios.transcoder.internal.utils.trackMapOf 8 | 9 | internal abstract class TranscodeEngine { 10 | 11 | abstract fun validate(): Boolean 12 | 13 | abstract fun transcode(progress: (Double) -> Unit) 14 | 15 | abstract fun cleanup() 16 | 17 | companion object { 18 | private val log = Logger("TranscodeEngine") 19 | 20 | private fun Throwable.isInterrupted(): Boolean { 21 | if (this is InterruptedException) return true 22 | if (this == this.cause) return false 23 | return this.cause?.isInterrupted() ?: false 24 | } 25 | 26 | @JvmStatic 27 | fun transcode(options: TranscoderOptions) { 28 | log.i("transcode(): called...") 29 | var engine: TranscodeEngine? = null 30 | val dispatcher = TranscodeDispatcher(options) 31 | try { 32 | engine = DefaultTranscodeEngine( 33 | dataSources = DataSources(options), 34 | dataSink = options.dataSink, 35 | strategies = trackMapOf( 36 | video = options.videoTrackStrategy, 37 | audio = options.audioTrackStrategy 38 | ), 39 | validator = options.validator, 40 | videoRotation = options.videoRotation, 41 | interpolator = options.timeInterpolator, 42 | audioStretcher = options.audioStretcher, 43 | audioResampler = options.audioResampler 44 | ) 45 | if (!engine.validate()) { 46 | dispatcher.dispatchSuccess(Transcoder.SUCCESS_NOT_NEEDED) 47 | } else { 48 | engine.transcode { 49 | dispatcher.dispatchProgress(it) 50 | } 51 | dispatcher.dispatchSuccess(Transcoder.SUCCESS_TRANSCODED) 52 | } 53 | } catch (e: Exception) { 54 | if (e.isInterrupted()) { 55 | log.i("Transcode canceled.", e) 56 | dispatcher.dispatchCancel() 57 | } else { 58 | log.e("Unexpected error while transcoding.", e) 59 | dispatcher.dispatchFailure(e) 60 | throw e 61 | } 62 | } finally { 63 | engine?.cleanup() 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/utils/AvcCsdUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 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.utils; 17 | 18 | import android.media.MediaFormat; 19 | 20 | import androidx.annotation.NonNull; 21 | 22 | import com.otaliastudios.transcoder.internal.media.MediaFormatConstants; 23 | 24 | import java.nio.ByteBuffer; 25 | import java.util.Arrays; 26 | 27 | public class AvcCsdUtils { 28 | // Refer: https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/MediaCodec.cpp#2198 29 | // Refer: http://stackoverflow.com/a/2861340 30 | private static final byte[] AVC_START_CODE_3 = {0x00, 0x00, 0x01}; 31 | private static final byte[] AVC_START_CODE_4 = {0x00, 0x00, 0x00, 0x01}; 32 | // Refer: http://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set/ 33 | private static final byte AVC_SPS_NAL = 103; // 0<<7 + 3<<5 + 7<<0 34 | // https://tools.ietf.org/html/rfc6184 35 | private static final byte AVC_SPS_NAL_2 = 39; // 0<<7 + 1<<5 + 7<<0 36 | private static final byte AVC_SPS_NAL_3 = 71; // 0<<7 + 2<<5 + 7<<0 37 | 38 | /** 39 | * @param format the input format 40 | * @return ByteBuffer contains SPS without NAL header. 41 | */ 42 | @NonNull 43 | public static ByteBuffer getSpsBuffer(@NonNull MediaFormat format) { 44 | ByteBuffer sourceBuffer = format.getByteBuffer(MediaFormatConstants.KEY_AVC_SPS).asReadOnlyBuffer(); // might be direct buffer 45 | ByteBuffer prefixedSpsBuffer = ByteBuffer.allocate(sourceBuffer.limit()).order(sourceBuffer.order()); 46 | prefixedSpsBuffer.put(sourceBuffer); 47 | prefixedSpsBuffer.flip(); 48 | 49 | skipStartCode(prefixedSpsBuffer); 50 | 51 | byte spsNalData = prefixedSpsBuffer.get(); 52 | if (spsNalData != AVC_SPS_NAL && spsNalData != AVC_SPS_NAL_2 && spsNalData != AVC_SPS_NAL_3) { 53 | throw new IllegalStateException("Got non SPS NAL data."); 54 | } 55 | 56 | return prefixedSpsBuffer.slice(); 57 | } 58 | 59 | private static void skipStartCode(@NonNull ByteBuffer prefixedSpsBuffer) { 60 | byte[] prefix3 = new byte[3]; 61 | prefixedSpsBuffer.get(prefix3); 62 | if (Arrays.equals(prefix3, AVC_START_CODE_3)) return; 63 | 64 | byte[] prefix4 = Arrays.copyOf(prefix3, 4); 65 | prefix4[3] = prefixedSpsBuffer.get(); 66 | if (Arrays.equals(prefix4, AVC_START_CODE_4)) return; 67 | throw new IllegalStateException("AVC NAL start code not found in csd."); 68 | } 69 | 70 | private AvcCsdUtils() { 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/utils/AvcSpsUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 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.utils; 17 | 18 | import androidx.annotation.NonNull; 19 | 20 | import java.nio.ByteBuffer; 21 | 22 | public class AvcSpsUtils { 23 | // Refer: http://en.wikipedia.org/wiki/H.264/MPEG-4_AVC#Profiles 24 | public static final byte PROFILE_IDC_BASELINE = 66; 25 | 26 | @SuppressWarnings("WeakerAccess") 27 | public static final byte PROFILE_IDC_EXTENDED = 88; 28 | 29 | @SuppressWarnings("WeakerAccess") 30 | public static final byte PROFILE_IDC_MAIN = 77; 31 | 32 | @SuppressWarnings("WeakerAccess") 33 | public static final byte PROFILE_IDC_HIGH = 100; 34 | 35 | public static byte getProfileIdc(@NonNull ByteBuffer spsBuffer) { 36 | // Refer: http://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set/ 37 | // First byte after NAL. 38 | return spsBuffer.get(0); 39 | } 40 | 41 | @NonNull 42 | public static String getProfileName(byte profileIdc) { 43 | switch (profileIdc) { 44 | case PROFILE_IDC_BASELINE: return "Baseline Profile"; 45 | case PROFILE_IDC_EXTENDED: return "Extended Profile"; 46 | case PROFILE_IDC_MAIN: return "Main Profile"; 47 | case PROFILE_IDC_HIGH: return "High Profile"; 48 | default: return "Unknown Profile (" + profileIdc + ")"; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/utils/BitRates.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.utils; 2 | 3 | import android.media.MediaFormat; 4 | 5 | /** 6 | * Utilities for bit rate estimation. 7 | */ 8 | public class BitRates { 9 | 10 | // For AVC this should be a reasonable default. 11 | // https://stackoverflow.com/a/5220554/4288782 12 | public static long estimateVideoBitRate(int width, int height, int frameRate) { 13 | return (long) (0.07F * 2 * width * height * frameRate); 14 | } 15 | 16 | // Wildly assuming a 0.75 compression rate for AAC. 17 | @SuppressWarnings("UnnecessaryLocalVariable") 18 | public static long estimateAudioBitRate(int channels, int sampleRate) { 19 | int bitsPerSample = 16; 20 | long samplesPerSecondPerChannel = (long) sampleRate; 21 | long bitsPerSecond = bitsPerSample * samplesPerSecondPerChannel * channels; 22 | double codecCompression = 0.75D; // Totally random. 23 | return (long) (bitsPerSecond * codecCompression); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/utils/ISO6709LocationParser.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.utils; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | public class ISO6709LocationParser { 9 | private final Pattern pattern; 10 | 11 | public ISO6709LocationParser() { 12 | this.pattern = Pattern.compile("([+\\-][0-9.]+)([+\\-][0-9.]+)"); 13 | } 14 | 15 | /** 16 | * This method parses the given string representing a geographic point location by coordinates in ISO 6709 format 17 | * and returns the latitude and the longitude in float. If location is not in ISO 6709 format, 18 | * this method returns null 19 | * 20 | * @param location a String representing a geographic point location by coordinates in ISO 6709 format 21 | * @return null if the given string is not as expected, an array of floats with size 2, 22 | * where the first element represents latitude and the second represents longitude, otherwise. 23 | */ 24 | @Nullable 25 | public float[] parse(@Nullable String location) { 26 | if (location == null) return null; 27 | Matcher m = pattern.matcher(location); 28 | if (m.find() && m.groupCount() == 2) { 29 | String latstr = m.group(1); 30 | String lonstr = m.group(2); 31 | try { 32 | float lat = Float.parseFloat(latstr); 33 | float lon = Float.parseFloat(lonstr); 34 | return new float[]{lat, lon}; 35 | } catch (NumberFormatException ignored) { 36 | } 37 | } 38 | return null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/utils/Logger.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.utils; 2 | 3 | import androidx.annotation.IntDef; 4 | import androidx.annotation.NonNull; 5 | import androidx.annotation.Nullable; 6 | import android.util.Log; 7 | 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | 11 | public class Logger { 12 | 13 | public final static int LEVEL_VERBOSE = 0; 14 | 15 | @SuppressWarnings("WeakerAccess") 16 | public final static int LEVEL_INFO = 1; 17 | 18 | @SuppressWarnings("WeakerAccess") 19 | public final static int LEVEL_WARNING = 2; 20 | 21 | @SuppressWarnings("WeakerAccess") 22 | public final static int LEVEL_ERROR = 3; 23 | 24 | private static int sLevel = LEVEL_INFO; 25 | 26 | /** 27 | * Interface of integers representing log levels. 28 | * @see #LEVEL_VERBOSE 29 | * @see #LEVEL_INFO 30 | * @see #LEVEL_WARNING 31 | * @see #LEVEL_ERROR 32 | */ 33 | @SuppressWarnings("WeakerAccess") 34 | @IntDef({LEVEL_VERBOSE, LEVEL_INFO, LEVEL_WARNING, LEVEL_ERROR}) 35 | @Retention(RetentionPolicy.SOURCE) 36 | public @interface LogLevel {} 37 | 38 | private final String mTag; 39 | private final int mLevel; 40 | 41 | public Logger(@NonNull String tag) { 42 | mTag = tag; 43 | mLevel = sLevel; 44 | } 45 | 46 | public Logger(@NonNull String tag, int level) { 47 | mTag = tag; 48 | mLevel = level; 49 | } 50 | 51 | /** 52 | * Sets the log sLevel for logcat events. 53 | * 54 | * @see #LEVEL_VERBOSE 55 | * @see #LEVEL_INFO 56 | * @see #LEVEL_WARNING 57 | * @see #LEVEL_ERROR 58 | * @param logLevel the desired log sLevel 59 | */ 60 | public static void setLogLevel(@LogLevel int logLevel) { 61 | sLevel = logLevel; 62 | } 63 | 64 | private boolean should(int messageLevel) { 65 | return mLevel <= messageLevel; 66 | } 67 | 68 | public void v(String message) { v(message, null); } 69 | 70 | public void i(String message) { i(message, null); } 71 | 72 | public void w(String message) { w(message, null); } 73 | 74 | public void e(String message) { e(message, null); } 75 | 76 | @SuppressWarnings("WeakerAccess") 77 | public void v(String message, @Nullable Throwable error) { 78 | log(LEVEL_VERBOSE, message, error); 79 | } 80 | 81 | public void i(String message, @Nullable Throwable error) { 82 | log(LEVEL_INFO, message, error); 83 | } 84 | 85 | public void w(String message, @Nullable Throwable error) { 86 | log(LEVEL_WARNING, message, error); 87 | } 88 | 89 | public void e(String message, @Nullable Throwable error) { 90 | log(LEVEL_ERROR, message, error); 91 | } 92 | 93 | private void log(int level, String message, @Nullable Throwable throwable) { 94 | if (!should(level)) return; 95 | switch (level) { 96 | case LEVEL_VERBOSE: Log.v(mTag, message, throwable); break; 97 | case LEVEL_INFO: Log.i(mTag, message, throwable); break; 98 | case LEVEL_WARNING: Log.w(mTag, message, throwable); break; 99 | case LEVEL_ERROR: Log.e(mTag, message, throwable); break; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/utils/ThreadPool.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.utils 2 | 3 | import java.util.concurrent.LinkedBlockingQueue 4 | import java.util.concurrent.ThreadFactory 5 | import java.util.concurrent.ThreadPoolExecutor 6 | import java.util.concurrent.TimeUnit 7 | import java.util.concurrent.atomic.AtomicInteger 8 | 9 | internal object ThreadPool { 10 | 11 | /** 12 | * NOTE: A better maximum pool size (instead of CPU+1) would be the number of MediaCodec 13 | * instances that the device can handle at the same time. Hard to tell though as that 14 | * also depends on the codec type / on input data. 15 | */ 16 | @JvmStatic 17 | val executor = ThreadPoolExecutor( 18 | Runtime.getRuntime().availableProcessors() + 1, 19 | Runtime.getRuntime().availableProcessors() + 1, 20 | 60, 21 | TimeUnit.SECONDS, 22 | LinkedBlockingQueue(), 23 | object : ThreadFactory { 24 | private val count = AtomicInteger(1) 25 | override fun newThread(r: Runnable): Thread { 26 | return Thread(r, "TranscoderThread #" + count.getAndIncrement()) 27 | } 28 | }) 29 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/utils/TrackMap.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.utils 2 | 3 | import com.otaliastudios.transcoder.common.TrackType 4 | 5 | interface TrackMap : Iterable { 6 | 7 | operator fun get(type: TrackType): T 8 | val video get() = get(TrackType.VIDEO) 9 | val audio get() = get(TrackType.AUDIO) 10 | 11 | fun has(type: TrackType): Boolean 12 | val hasVideo get() = has(TrackType.VIDEO) 13 | val hasAudio get() = has(TrackType.AUDIO) 14 | 15 | fun getOrNull(type: TrackType) = if (has(type)) get(type) else null 16 | fun videoOrNull() = getOrNull(TrackType.VIDEO) 17 | fun audioOrNull() = getOrNull(TrackType.AUDIO) 18 | 19 | val size get() = listOfNotNull(videoOrNull(), audioOrNull()).size 20 | 21 | override fun iterator() = listOfNotNull(videoOrNull(), audioOrNull()).iterator() 22 | } 23 | 24 | interface MutableTrackMap : TrackMap { 25 | operator fun set(type: TrackType, value: T?) 26 | 27 | fun reset(video: T?, audio: T?) { 28 | set(TrackType.VIDEO, video) 29 | set(TrackType.AUDIO, audio) 30 | } 31 | 32 | override var audio: T 33 | get() = super.audio 34 | set(value) = set(TrackType.AUDIO, value) 35 | 36 | override var video: T 37 | get() = super.video 38 | set(value) = set(TrackType.VIDEO, value) 39 | } 40 | 41 | fun trackMapOf(default: T?) = trackMapOf(default, default) 42 | 43 | fun trackMapOf(video: T?, audio: T?): TrackMap = DefaultTrackMap(video, audio) 44 | 45 | fun mutableTrackMapOf(default: T?) = mutableTrackMapOf(default, default) 46 | 47 | fun mutableTrackMapOf(video: T? = null, audio: T? = null): MutableTrackMap = DefaultTrackMap(video, audio) 48 | 49 | private class DefaultTrackMap(video: T?, audio: T?) : MutableTrackMap { 50 | private val map = mutableMapOf(TrackType.VIDEO to video, TrackType.AUDIO to audio) 51 | override fun get(type: TrackType): T = requireNotNull(map[type]) 52 | override fun has(type: TrackType) = map[type] != null 53 | override fun set(type: TrackType, value: T?) { 54 | map[type] = value 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/utils/debug.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.utils 2 | 3 | internal fun stackTrace() = Thread.currentThread().stackTrace.drop(2).take(10).joinToString("\n") -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/utils/eos.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.utils 2 | 3 | import android.media.MediaCodec 4 | import com.otaliastudios.transcoder.common.TrackType 5 | import com.otaliastudios.transcoder.internal.Segment 6 | import com.otaliastudios.transcoder.internal.Segments 7 | import com.otaliastudios.transcoder.sink.DataSink 8 | import com.otaliastudios.transcoder.source.DataSource 9 | import java.nio.ByteBuffer 10 | 11 | // See https://github.com/natario1/Transcoder/issues/107 12 | internal fun DataSink.ignoringEos(ignore: () -> Boolean): DataSink 13 | = EosIgnoringDataSink(this, ignore) 14 | 15 | private class EosIgnoringDataSink( 16 | private val sink: DataSink, 17 | private val ignore: () -> Boolean, 18 | ) : DataSink by sink { 19 | private val info = MediaCodec.BufferInfo() 20 | override fun writeTrack(type: TrackType, byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) { 21 | if (ignore()) { 22 | val flags = bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM.inv() 23 | if (bufferInfo.size > 0 || flags != 0) { 24 | info.set(bufferInfo.offset, bufferInfo.size, bufferInfo.presentationTimeUs, flags) 25 | sink.writeTrack(type, byteBuffer, info) 26 | } 27 | } else { 28 | sink.writeTrack(type, byteBuffer, bufferInfo) 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * When transcoding has more tracks, we need to check if we need to force EOS on some of them. 35 | * This can happen if the user adds e.g. 1 minute of audio with 20 seconds of video. 36 | * In this case the video track must be stopped once the audio stops. 37 | */ 38 | internal fun DataSource.forcingEos(force: () -> Boolean): DataSource 39 | = EosForcingDataSource(this, force) 40 | 41 | private class EosForcingDataSource( 42 | private val source: DataSource, 43 | private val force: () -> Boolean, 44 | ) : DataSource by source { 45 | override fun isDrained(): Boolean { 46 | return force() || source.isDrained 47 | } 48 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/video/FrameDropper.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.video 2 | 3 | import com.otaliastudios.transcoder.internal.utils.Logger 4 | 5 | internal interface FrameDropper { 6 | fun shouldRender(timeUs: Long): Boolean 7 | } 8 | 9 | /** 10 | * A very simple dropper, from 11 | * https://stackoverflow.com/questions/4223766/dropping-video-frames 12 | */ 13 | internal fun FrameDropper(inputFps: Int, outputFps: Int) = object : FrameDropper { 14 | 15 | private val log = Logger("FrameDropper") 16 | private val inputSpf = 1.0 / inputFps 17 | private val outputSpf = 1.0 / outputFps 18 | private var currentSpf = 0.0 19 | private var frameCount = 0 20 | 21 | override fun shouldRender(timeUs: Long): Boolean { 22 | currentSpf += inputSpf 23 | if (frameCount++ == 0) { 24 | log.v("RENDERING (first frame) - currentSpf=$currentSpf inputSpf=$inputSpf outputSpf=$outputSpf") 25 | return true 26 | } else if (currentSpf > outputSpf) { 27 | currentSpf -= outputSpf 28 | log.v("RENDERING - currentSpf=$currentSpf inputSpf=$inputSpf outputSpf=$outputSpf") 29 | return true 30 | } else { 31 | log.v("DROPPING - currentSpf=$currentSpf inputSpf=$inputSpf outputSpf=$outputSpf") 32 | return false 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoPublisher.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.video 2 | 3 | import android.opengl.EGL14 4 | import com.otaliastudios.opengl.core.EglCore 5 | import com.otaliastudios.opengl.surface.EglWindowSurface 6 | import com.otaliastudios.transcoder.internal.codec.EncoderChannel 7 | import com.otaliastudios.transcoder.internal.codec.EncoderData 8 | import com.otaliastudios.transcoder.internal.pipeline.BaseStep 9 | import com.otaliastudios.transcoder.internal.pipeline.Channel 10 | import com.otaliastudios.transcoder.internal.pipeline.State 11 | import com.otaliastudios.transcoder.internal.pipeline.Step 12 | 13 | 14 | internal class VideoPublisher: BaseStep("VideoPublisher") { 15 | 16 | override val channel = Channel 17 | 18 | override fun advance(state: State.Ok): State { 19 | if (state is State.Eos) { 20 | return State.Eos(EncoderData.Empty) 21 | } else { 22 | val surface = next.surface!! 23 | surface.window.setPresentationTime(state.value * 1000) 24 | surface.window.swapBuffers() 25 | /* val s = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW) 26 | val ss = IntArray(2) 27 | EGL14.eglQuerySurface(EGL14.eglGetCurrentDisplay(), s, EGL14.EGL_WIDTH, ss, 0) 28 | EGL14.eglQuerySurface(EGL14.eglGetCurrentDisplay(), s, EGL14.EGL_HEIGHT, ss, 1) 29 | log.e("XXX VideoPublisher.surfaceSize: ${ss[0]}x${ss[1]}") */ 30 | return State.Ok(EncoderData.Empty) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/internal/video/VideoSnapshots.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.internal.video 2 | 3 | import android.graphics.Bitmap 4 | import android.media.MediaFormat 5 | import android.media.MediaFormat.KEY_HEIGHT 6 | import android.media.MediaFormat.KEY_WIDTH 7 | import android.opengl.EGL14 8 | import android.opengl.GLES20 9 | import com.otaliastudios.opengl.core.EglCore 10 | import com.otaliastudios.opengl.core.Egloo 11 | import com.otaliastudios.opengl.surface.EglOffscreenSurface 12 | import com.otaliastudios.opengl.surface.EglSurface 13 | import com.otaliastudios.transcoder.internal.pipeline.BaseStep 14 | import com.otaliastudios.transcoder.internal.pipeline.Channel 15 | import com.otaliastudios.transcoder.internal.pipeline.State 16 | import com.otaliastudios.transcoder.internal.utils.Logger 17 | import com.otaliastudios.transcoder.thumbnail.Thumbnail 18 | import java.nio.ByteBuffer 19 | import java.nio.ByteOrder 20 | import kotlin.math.abs 21 | 22 | internal class VideoSnapshots( 23 | format: MediaFormat, 24 | requests: List, 25 | private val accuracyUs: Long, 26 | private val onSnapshot: (Long, Bitmap) -> Unit 27 | ) : BaseStep("VideoSnapshots") { 28 | 29 | override val channel = Channel 30 | private val requests = requests.toMutableList() 31 | private val width = format.getInteger(KEY_WIDTH) 32 | private val height = format.getInteger(KEY_HEIGHT) 33 | private val core = EglCore(EGL14.EGL_NO_CONTEXT, EglCore.FLAG_RECORDABLE) 34 | private val surface = EglOffscreenSurface(core, width, height).also { 35 | it.makeCurrent() 36 | } 37 | 38 | override fun advance(state: State.Ok): State { 39 | if (requests.isEmpty()) return state 40 | 41 | val expectedUs = requests.first() 42 | val deltaUs = abs(expectedUs - state.value) 43 | if (deltaUs < accuracyUs || (state is State.Eos && expectedUs > state.value)) { 44 | log.i("Request MATCHED! expectedUs=$expectedUs actualUs=${state.value} deltaUs=$deltaUs") 45 | requests.removeFirst() 46 | val buffer = ByteBuffer.allocateDirect(width * height * 4) 47 | buffer.order(ByteOrder.LITTLE_ENDIAN) 48 | GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer) 49 | Egloo.checkGlError("glReadPixels") 50 | buffer.rewind() 51 | val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 52 | bitmap.copyPixelsFromBuffer(buffer) 53 | onSnapshot(state.value, bitmap) 54 | } else { 55 | log.v("Request has high delta. expectedUs=$expectedUs actualUs=${state.value} deltaUs=$deltaUs") 56 | } 57 | return state 58 | } 59 | 60 | override fun release() { 61 | surface.release() 62 | core.release() 63 | } 64 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/resample/AudioResampler.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.resample; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.nio.Buffer; 6 | import java.nio.ShortBuffer; 7 | 8 | /** 9 | * Resamples audio data. See {@link UpsampleAudioResampler} or 10 | * {@link DownsampleAudioResampler} for concrete implementations. 11 | */ 12 | public interface AudioResampler { 13 | 14 | /** 15 | * Resamples input audio from input buffer into the output buffer. 16 | * 17 | * @param inputBuffer the input buffer 18 | * @param inputSampleRate the input sample rate 19 | * @param outputBuffer the output buffer 20 | * @param outputSampleRate the output sample rate 21 | * @param channels the number of channels 22 | */ 23 | void resample(@NonNull final ShortBuffer inputBuffer, int inputSampleRate, @NonNull final ShortBuffer outputBuffer, int outputSampleRate, int channels); 24 | 25 | AudioResampler DOWNSAMPLE = new DownsampleAudioResampler(); 26 | 27 | AudioResampler UPSAMPLE = new UpsampleAudioResampler(); 28 | 29 | AudioResampler PASSTHROUGH = new PassThroughAudioResampler(); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/resample/DefaultAudioResampler.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.resample; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.nio.ShortBuffer; 6 | 7 | /** 8 | * An {@link AudioResampler} that delegates to appropriate classes 9 | * based on input and output size. 10 | */ 11 | public class DefaultAudioResampler implements AudioResampler { 12 | 13 | @Override 14 | public void resample(@NonNull ShortBuffer inputBuffer, int inputSampleRate, @NonNull ShortBuffer outputBuffer, int outputSampleRate, int channels) { 15 | if (inputSampleRate < outputSampleRate) { 16 | UPSAMPLE.resample(inputBuffer, inputSampleRate, outputBuffer, outputSampleRate, channels); 17 | } else if (inputSampleRate > outputSampleRate) { 18 | DOWNSAMPLE.resample(inputBuffer, inputSampleRate, outputBuffer, outputSampleRate, channels); 19 | } else { 20 | PASSTHROUGH.resample(inputBuffer, inputSampleRate, outputBuffer, outputSampleRate, channels); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/resample/DownsampleAudioResampler.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.resample; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.nio.ShortBuffer; 6 | 7 | /** 8 | * An {@link AudioResampler} that downsamples from a higher sample rate to a lower sample rate. 9 | */ 10 | public class DownsampleAudioResampler implements AudioResampler { 11 | 12 | private static float ratio(int remaining, int all) { 13 | return (float) remaining / all; 14 | } 15 | 16 | @Override 17 | public void resample(@NonNull ShortBuffer inputBuffer, int inputSampleRate, @NonNull ShortBuffer outputBuffer, int outputSampleRate, int channels) { 18 | if (inputSampleRate < outputSampleRate) { 19 | throw new IllegalArgumentException("Illegal use of DownsampleAudioResampler"); 20 | } 21 | if (channels != 1 && channels != 2) { 22 | throw new IllegalArgumentException("Illegal use of DownsampleAudioResampler. Channels:" + channels); 23 | } 24 | final int inputSamples = inputBuffer.remaining() / channels; 25 | final int outputSamples = (int) Math.ceil(inputSamples * ((double) outputSampleRate / inputSampleRate)); 26 | final int dropSamples = inputSamples - outputSamples; 27 | int remainingOutputSamples = outputSamples; 28 | int remainingDropSamples = dropSamples; 29 | float remainingOutputSamplesRatio = ratio(remainingOutputSamples, outputSamples); 30 | float remainingDropSamplesRatio = ratio(remainingDropSamples, dropSamples); 31 | while (remainingOutputSamples > 0 && remainingDropSamples > 0) { 32 | // Will this be an input sample or a drop sample? 33 | // Choose the one with the bigger ratio. 34 | if (remainingOutputSamplesRatio >= remainingDropSamplesRatio) { 35 | outputBuffer.put(inputBuffer.get()); 36 | if (channels == 2) outputBuffer.put(inputBuffer.get()); 37 | remainingOutputSamples--; 38 | remainingOutputSamplesRatio = ratio(remainingOutputSamples, outputSamples); 39 | } else { 40 | // Drop this - read from input without writing. 41 | inputBuffer.position(inputBuffer.position() + channels); 42 | remainingDropSamples--; 43 | remainingDropSamplesRatio = ratio(remainingDropSamples, dropSamples); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/resample/PassThroughAudioResampler.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.resample; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.nio.ShortBuffer; 6 | 7 | /** 8 | * An {@link AudioResampler} that does nothing, meant to be used when sample 9 | * rates are identical. 10 | */ 11 | public class PassThroughAudioResampler implements AudioResampler { 12 | 13 | @Override 14 | public void resample(@NonNull ShortBuffer inputBuffer, int inputSampleRate, 15 | @NonNull ShortBuffer outputBuffer, int outputSampleRate, int channels) { 16 | if (inputSampleRate != outputSampleRate) { 17 | throw new IllegalArgumentException("Illegal use of PassThroughAudioResampler"); 18 | } 19 | outputBuffer.put(inputBuffer); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/resample/UpsampleAudioResampler.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.resample; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.nio.ShortBuffer; 6 | 7 | /** 8 | * An {@link AudioResampler} that upsamples from a lower sample rate to a higher sample rate. 9 | */ 10 | public class UpsampleAudioResampler implements AudioResampler { 11 | 12 | private static float ratio(int remaining, int all) { 13 | return (float) remaining / all; 14 | } 15 | 16 | @Override 17 | public void resample(@NonNull ShortBuffer inputBuffer, int inputSampleRate, @NonNull ShortBuffer outputBuffer, int outputSampleRate, int channels) { 18 | if (inputSampleRate > outputSampleRate) { 19 | throw new IllegalArgumentException("Illegal use of UpsampleAudioResampler"); 20 | } 21 | if (channels != 1 && channels != 2) { 22 | throw new IllegalArgumentException("Illegal use of UpsampleAudioResampler. Channels:" + channels); 23 | } 24 | final int inputSamples = inputBuffer.remaining() / channels; 25 | final int outputSamples = (int) Math.ceil(inputSamples * ((double) outputSampleRate / inputSampleRate)); 26 | final int fakeSamples = outputSamples - inputSamples; 27 | int remainingInputSamples = inputSamples; 28 | int remainingFakeSamples = fakeSamples; 29 | float remainingInputSamplesRatio = ratio(remainingInputSamples, inputSamples); 30 | float remainingFakeSamplesRatio = ratio(remainingFakeSamples, fakeSamples); 31 | while (remainingInputSamples > 0 && remainingFakeSamples > 0) { 32 | // Will this be an input sample or a fake sample? 33 | // Choose the one with the bigger ratio. 34 | if (remainingInputSamplesRatio >= remainingFakeSamplesRatio) { 35 | outputBuffer.put(inputBuffer.get()); 36 | if (channels == 2) outputBuffer.put(inputBuffer.get()); 37 | remainingInputSamples--; 38 | remainingInputSamplesRatio = ratio(remainingInputSamples, inputSamples); 39 | } else { 40 | outputBuffer.put(fakeSample(outputBuffer, inputBuffer, 1, channels)); 41 | if (channels == 2) outputBuffer.put(fakeSample(outputBuffer, inputBuffer, 2, channels)); 42 | remainingFakeSamples--; 43 | remainingFakeSamplesRatio = ratio(remainingFakeSamples, fakeSamples); 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * We have different options here. 50 | * 1. Return a 0 sample. 51 | * 2. Return a noise sample. 52 | * 3. Return the previous sample for this channel. 53 | * 4. Return an interpolated value between previous and next sample for this channel. 54 | * 55 | * None of this provides a real quality improvement, since the fundamental issue is that we 56 | * can not achieve a higher quality by simply inserting fake samples each X input samples. 57 | * A real upsampling algorithm should do something more intensive like interpolating between 58 | * all values, not just the spare one. 59 | * 60 | * However this is probably beyond the scope of this library. 61 | * 62 | * @param output output buffer 63 | * @param input output buffer 64 | * @param channel current channel 65 | * @param channels number of channels 66 | * @return a fake sample 67 | */ 68 | @SuppressWarnings("unused") 69 | private static short fakeSample(@NonNull ShortBuffer output, @NonNull ShortBuffer input, int channel, int channels) { 70 | return output.get(output.position() - channels); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/resize/AspectRatioResizer.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.resize; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.Size; 6 | import com.otaliastudios.transcoder.resize.Resizer; 7 | 8 | /** 9 | * A {@link Resizer} that crops the input size to match the given 10 | * aspect ratio, respecting the source portrait or landscape-ness. 11 | */ 12 | public class AspectRatioResizer implements Resizer { 13 | 14 | private final float aspectRatio; 15 | 16 | /** 17 | * Creates a new resizer. 18 | * @param aspectRatio the desired aspect ratio 19 | */ 20 | public AspectRatioResizer(float aspectRatio) { 21 | this.aspectRatio = aspectRatio; 22 | } 23 | 24 | @NonNull 25 | @Override 26 | public Size getOutputSize(@NonNull Size inputSize) { 27 | float inputRatio = (float) inputSize.getMajor() / inputSize.getMinor(); 28 | float outputRatio = aspectRatio > 1 ? aspectRatio : 1F / aspectRatio; 29 | // now both are greater than 1 (major / minor). 30 | if (inputRatio > outputRatio) { 31 | // input is "wider". We must reduce the input major dimension. 32 | return new Size(inputSize.getMinor(), (int) (outputRatio * inputSize.getMinor())); 33 | } else if (inputRatio < outputRatio) { 34 | // input is more square. We must reduce the input minor dimension. 35 | return new Size(inputSize.getMajor(), (int) (inputSize.getMajor() / outputRatio)); 36 | } else { 37 | return inputSize; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/resize/AtMostResizer.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.resize; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.Size; 6 | import com.otaliastudios.transcoder.resize.Resizer; 7 | 8 | /** 9 | * A {@link Resizer} that scales down the input size so that its dimension 10 | * is smaller or equal to a certain value. 11 | */ 12 | public class AtMostResizer implements Resizer { 13 | 14 | private final int atMostMinor; 15 | private final int atMostMajor; 16 | 17 | /** 18 | * Checks just the minor dimension. 19 | * @param atMost the dimension constraint 20 | */ 21 | public AtMostResizer(int atMost) { 22 | atMostMinor = atMost; 23 | atMostMajor = Integer.MAX_VALUE; 24 | } 25 | 26 | /** 27 | * Checks both dimensions. 28 | * @param atMostMinor the minor dimension constraint 29 | * @param atMostMajor the major dimension constraint 30 | */ 31 | public AtMostResizer(int atMostMinor, int atMostMajor) { 32 | this.atMostMinor = atMostMinor; 33 | this.atMostMajor = atMostMajor; 34 | } 35 | 36 | @NonNull 37 | @Override 38 | public Size getOutputSize(@NonNull Size inputSize) { 39 | if (inputSize.getMinor() <= atMostMinor && inputSize.getMajor() <= atMostMajor) { 40 | // No compression needed here. 41 | return inputSize; 42 | } 43 | int outMinor, outMajor; 44 | float minorScale = (float) inputSize.getMinor() / atMostMinor; // approx. 0 if not needed 45 | float maiorScale = (float) inputSize.getMajor() / atMostMajor; // > 1 if needed. 46 | float inputRatio = (float) inputSize.getMinor() / inputSize.getMajor(); 47 | if (maiorScale >= minorScale) { 48 | outMajor = atMostMajor; 49 | outMinor = (int) ((float) outMajor * inputRatio); 50 | } else { 51 | outMinor = atMostMinor; 52 | outMajor = (int) ((float) outMinor / inputRatio); 53 | } 54 | if (outMinor % 2 != 0) outMinor--; 55 | if (outMajor % 2 != 0) outMajor--; 56 | return new Size(outMinor, outMajor); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/resize/ExactResizer.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.resize; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.Size; 6 | import com.otaliastudios.transcoder.resize.Resizer; 7 | 8 | /** 9 | * A {@link Resizer} that returns the exact dimensions that were passed to the constructor. 10 | */ 11 | public class ExactResizer implements Resizer { 12 | 13 | private final Size output; 14 | 15 | public ExactResizer(int first, int second) { 16 | output = new Size(first, second); 17 | } 18 | 19 | @SuppressWarnings("unused") 20 | public ExactResizer(@NonNull Size size) { 21 | output = size; 22 | } 23 | 24 | @NonNull 25 | @Override 26 | public Size getOutputSize(@NonNull Size inputSize) { 27 | // We now support different aspect ratios, but could make this check below configurable. 28 | /* if (inputSize.getMinor() * output.getMajor() != inputSize.getMajor() * output.getMinor()) { 29 | throw new IllegalStateException("Input and output ratio do not match."); 30 | } */ 31 | return output; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/resize/FractionResizer.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.resize; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.Size; 6 | import com.otaliastudios.transcoder.resize.Resizer; 7 | 8 | /** 9 | * A {@link Resizer} that reduces the input size by the given fraction. 10 | * This ensures that output dimensions are not an odd number (refused by a few codecs). 11 | */ 12 | public class FractionResizer implements Resizer { 13 | 14 | private final float fraction; 15 | 16 | public FractionResizer(float fraction) { 17 | if (fraction <= 0 || fraction > 1) { 18 | throw new IllegalArgumentException("Fraction must be > 0 and <= 1"); 19 | } 20 | this.fraction = fraction; 21 | } 22 | 23 | @NonNull 24 | @Override 25 | public Size getOutputSize(@NonNull Size inputSize) { 26 | int minor = (int) (fraction * inputSize.getMinor()); 27 | int major = (int) (fraction * inputSize.getMajor()); 28 | if (minor % 2 != 0) minor--; 29 | if (major % 2 != 0) major--; 30 | return new Size(minor, major); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/resize/MultiResizer.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.resize; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import com.otaliastudios.transcoder.common.Size; 9 | import com.otaliastudios.transcoder.resize.Resizer; 10 | 11 | /** 12 | * A {@link Resizer} that applies a chain of multiple resizers. 13 | * Of course order matters: the output of a resizer is the input of the next one. 14 | */ 15 | public class MultiResizer implements Resizer { 16 | 17 | private final List list = new ArrayList<>(); 18 | 19 | // In this case we act as a pass through 20 | public MultiResizer() {} 21 | 22 | @SuppressWarnings("unused") 23 | public MultiResizer(@NonNull Resizer... resizers) { 24 | for (Resizer resizer : resizers) { 25 | addResizer(resizer); 26 | } 27 | } 28 | 29 | public void addResizer(@NonNull Resizer resizer) { 30 | list.add(resizer); 31 | } 32 | 33 | @NonNull 34 | @Override 35 | public Size getOutputSize(@NonNull Size inputSize) throws Exception { 36 | Size size = inputSize; 37 | for (Resizer resizer : list) { 38 | size = resizer.getOutputSize(size); 39 | } 40 | return size; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/resize/PassThroughResizer.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.resize; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.Size; 6 | import com.otaliastudios.transcoder.resize.Resizer; 7 | 8 | /** 9 | * A {@link Resizer} that returns the input size unchanged. 10 | */ 11 | public class PassThroughResizer implements Resizer { 12 | 13 | public PassThroughResizer() { } 14 | 15 | @NonNull 16 | @Override 17 | public Size getOutputSize(@NonNull Size inputSize) { 18 | return inputSize; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/resize/Resizer.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.resize; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.ExactSize; 6 | import com.otaliastudios.transcoder.common.Size; 7 | 8 | /** 9 | * A general purpose interface that can be used (accepted as a parameter) 10 | * by video strategies such as {@link com.otaliastudios.transcoder.strategy.DefaultVideoStrategy} 11 | * to compute the output size. 12 | * 13 | * Note that a {@link Size} itself has no notion of which dimension is width and which is height. 14 | * The video strategy that consumes this resizer will check the input orientation (portrait / landscape) 15 | * so that they match. 16 | * 17 | * To avoid this behavior and set exact width and height, instances can return an {@link ExactSize}. 18 | * In this case, width and height will be used as defined without checking for portrait / landscapeness 19 | * of input. 20 | * 21 | * However, the final displayed video might be rotated because it might have a non-zero rotation tag 22 | * in metadata (this is frequently the case). 23 | */ 24 | public interface Resizer { 25 | 26 | /** 27 | * Parses the input size and returns the output. 28 | * This method should throw an exception if the input size is not processable. 29 | * @param inputSize the input video size 30 | * @return the output video size 31 | * @throws Exception if something is wrong with input size 32 | */ 33 | @NonNull 34 | Size getOutputSize(@NonNull Size inputSize) throws Exception; 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/sink/DataSink.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.sink; 2 | 3 | import android.media.MediaCodec; 4 | import android.media.MediaFormat; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import com.otaliastudios.transcoder.common.TrackStatus; 9 | import com.otaliastudios.transcoder.common.TrackType; 10 | 11 | import java.nio.ByteBuffer; 12 | 13 | /** 14 | * A DataSink is an abstract representation of an encoded data collector. 15 | * Currently the only implementation is {@link DefaultDataSink} which collects 16 | * data into a {@link java.io.File} using {@link android.media.MediaMuxer}. 17 | * 18 | * However there might be other implementations in the future, for example to stream data 19 | * to a server. 20 | */ 21 | public interface DataSink { 22 | 23 | /** 24 | * Called before starting to set the orientation metadata. 25 | * @param orientation 0, 90, 180 or 270 26 | */ 27 | void setOrientation(int orientation); 28 | 29 | /** 30 | * Called before starting to set the location metadata. 31 | * @param latitude latitude 32 | * @param longitude longitude 33 | */ 34 | void setLocation(double latitude, double longitude); 35 | 36 | /** 37 | * Called before starting to set the status for the given 38 | * track. The sink object can check if the track is transcoding 39 | * using {@link TrackStatus#isTranscoding()}. 40 | * 41 | * @param type track type 42 | * @param status status 43 | */ 44 | void setTrackStatus(@NonNull TrackType type, 45 | @NonNull TrackStatus status); 46 | 47 | /** 48 | * Called by the transcoding pipeline when we have an output format. 49 | * This is not the output format chosen by the library user but rather the 50 | * output format determined by {@link MediaCodec}, which contains more information, 51 | * and should be inspected to know what kind of data we're collecting. 52 | * @param type the track type 53 | * @param format the track format 54 | */ 55 | void setTrackFormat(@NonNull TrackType type, 56 | @NonNull MediaFormat format); 57 | 58 | /** 59 | * Called by the transcoding pipeline to write data into this sink. 60 | * @param type the track type 61 | * @param byteBuffer the data 62 | * @param bufferInfo the metadata 63 | */ 64 | void writeTrack(@NonNull TrackType type, 65 | @NonNull ByteBuffer byteBuffer, 66 | @NonNull MediaCodec.BufferInfo bufferInfo); 67 | 68 | /** 69 | * Called when transcoders have stopped writing. 70 | */ 71 | void stop(); 72 | 73 | /** 74 | * Called to release resources. 75 | * Any exception should probably be caught here. 76 | */ 77 | void release(); 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/sink/DefaultDataSinkChecks.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 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.sink; 17 | 18 | import android.media.MediaFormat; 19 | 20 | import com.otaliastudios.transcoder.common.TrackType; 21 | import com.otaliastudios.transcoder.internal.utils.Logger; 22 | import com.otaliastudios.transcoder.internal.media.MediaFormatConstants; 23 | import com.otaliastudios.transcoder.internal.utils.AvcCsdUtils; 24 | import com.otaliastudios.transcoder.internal.utils.AvcSpsUtils; 25 | 26 | import java.nio.ByteBuffer; 27 | 28 | import androidx.annotation.NonNull; 29 | 30 | class DefaultDataSinkChecks { 31 | private static final Logger LOG = new Logger("DefaultDataSinkChecks"); 32 | 33 | void checkOutputFormat(@NonNull TrackType type, @NonNull MediaFormat format) { 34 | if (type == TrackType.VIDEO) { 35 | checkVideoOutputFormat(format); 36 | } else if (type == TrackType.AUDIO) { 37 | checkAudioOutputFormat(format); 38 | } 39 | } 40 | 41 | private void checkVideoOutputFormat(@NonNull MediaFormat format) { 42 | String mime = format.getString(MediaFormat.KEY_MIME); 43 | // Refer: http://developer.android.com/guide/appendix/media-formats.html#core 44 | // Refer: http://en.wikipedia.org/wiki/MPEG-4_Part_14#Data_streams 45 | if (!MediaFormatConstants.MIMETYPE_VIDEO_AVC.equals(mime)) { 46 | throw new InvalidOutputFormatException("Video codecs other than AVC is not supported, actual mime type: " + mime); 47 | } 48 | 49 | // The original lib by ypresto was throwing when detected a non-baseline profile. 50 | // But recent Android versions appear to have at least Main Profile support, although it's still 51 | // not enforced by Android CDD. See 2016 comment by Google employee (about decoding): 52 | // https://github.com/google/ExoPlayer/issues/1952#issuecomment-254206222 53 | // So instead of throwing, we prefer to just log the profile name and let the device try to handle. 54 | ByteBuffer spsBuffer = AvcCsdUtils.getSpsBuffer(format); 55 | byte profileIdc = AvcSpsUtils.getProfileIdc(spsBuffer); 56 | String profileName = AvcSpsUtils.getProfileName(profileIdc); 57 | if (profileIdc == AvcSpsUtils.PROFILE_IDC_BASELINE) { 58 | LOG.i("Output H.264 profile: " + profileName); 59 | } else { 60 | LOG.w("Output H.264 profile: " + profileName + ". This might not be supported."); 61 | } 62 | } 63 | 64 | private void checkAudioOutputFormat(@NonNull MediaFormat format) { 65 | String mime = format.getString(MediaFormat.KEY_MIME); 66 | if (!MediaFormatConstants.MIMETYPE_AUDIO_AAC.equals(mime)) { 67 | throw new InvalidOutputFormatException("Audio codecs other than AAC is not supported, actual mime type: " + mime); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/sink/InvalidOutputFormatException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 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.sink; 17 | 18 | import androidx.annotation.NonNull; 19 | 20 | import com.otaliastudios.transcoder.Transcoder; 21 | import com.otaliastudios.transcoder.TranscoderListener; 22 | import com.otaliastudios.transcoder.TranscoderOptions; 23 | 24 | /** 25 | * One of the exceptions possibly thrown by 26 | * {@link Transcoder#transcode(TranscoderOptions)}, which means it can be 27 | * passed to {@link TranscoderListener#onTranscodeFailed(Throwable)}. 28 | */ 29 | public class InvalidOutputFormatException extends RuntimeException { 30 | InvalidOutputFormatException(@NonNull String detailMessage) { 31 | super(detailMessage); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/sink/MultiDataSink.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.sink; 2 | 3 | import android.media.MediaCodec; 4 | import android.media.MediaFormat; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import com.otaliastudios.transcoder.common.TrackStatus; 9 | import com.otaliastudios.transcoder.common.TrackType; 10 | 11 | import java.nio.ByteBuffer; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | public class MultiDataSink implements DataSink { 16 | 17 | private final List sinks; 18 | 19 | public MultiDataSink(@NonNull DataSink... sink) { 20 | sinks = Arrays.asList(sink); 21 | } 22 | 23 | @Override 24 | public void setOrientation(int orientation) { 25 | for (DataSink sink : sinks) { 26 | sink.setOrientation(orientation); 27 | } 28 | } 29 | 30 | @Override 31 | public void setLocation(double latitude, double longitude) { 32 | for (DataSink sink : sinks) { 33 | sink.setLocation(latitude, longitude); 34 | } 35 | } 36 | 37 | @Override 38 | public void setTrackStatus(@NonNull TrackType type, @NonNull TrackStatus status) { 39 | for (DataSink sink : sinks) { 40 | sink.setTrackStatus(type, status); 41 | } 42 | } 43 | 44 | @Override 45 | public void setTrackFormat(@NonNull TrackType type, @NonNull MediaFormat format) { 46 | for (DataSink sink : sinks) { 47 | sink.setTrackFormat(type, format); 48 | } 49 | } 50 | 51 | @Override 52 | public void writeTrack(@NonNull TrackType type, @NonNull ByteBuffer byteBuffer, @NonNull MediaCodec.BufferInfo bufferInfo) { 53 | int position = byteBuffer.position(); 54 | int limit = byteBuffer.limit(); 55 | for (DataSink sink : sinks) { 56 | sink.writeTrack(type, byteBuffer, bufferInfo); 57 | byteBuffer.position(position); 58 | byteBuffer.limit(limit); 59 | } 60 | } 61 | 62 | @Override 63 | public void stop() { 64 | for (DataSink sink : sinks) { 65 | sink.stop(); 66 | } 67 | } 68 | 69 | @Override 70 | public void release() { 71 | for (DataSink sink : sinks) { 72 | sink.release(); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/source/AssetFileDescriptorDataSource.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.source; 2 | 3 | import android.content.res.AssetFileDescriptor; 4 | import android.media.MediaExtractor; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import java.io.FileInputStream; 9 | import java.io.IOException; 10 | 11 | /** 12 | * It is the caller responsibility to close the file descriptor. 13 | */ 14 | public class AssetFileDescriptorDataSource extends DataSourceWrapper { 15 | public AssetFileDescriptorDataSource(@NonNull AssetFileDescriptor assetFileDescriptor) { 16 | super(new FileDescriptorDataSource( 17 | assetFileDescriptor.getFileDescriptor(), 18 | assetFileDescriptor.getStartOffset(), 19 | assetFileDescriptor.getDeclaredLength() 20 | )); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/source/ClipDataSource.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.source; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | /** 6 | * A {@link DataSource} that clips the inner source within the given interval. 7 | */ 8 | @SuppressWarnings("unused") 9 | public class ClipDataSource extends DataSourceWrapper { 10 | 11 | public ClipDataSource(@NonNull DataSource source, long clipStartUs) { 12 | super(new TrimDataSource(source, clipStartUs)); 13 | } 14 | 15 | public ClipDataSource(@NonNull DataSource source, long clipStartUs, long clipEndUs) { 16 | super(new TrimDataSource(source, 17 | clipStartUs, 18 | getSourceDurationUs(source) - clipEndUs)); 19 | } 20 | 21 | private static long getSourceDurationUs(@NonNull DataSource source) { 22 | if (!source.isInitialized()) source.initialize(); 23 | return source.getDurationUs(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.source; 2 | 3 | import android.media.MediaFormat; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | 8 | import com.otaliastudios.transcoder.common.TrackType; 9 | 10 | import java.nio.ByteBuffer; 11 | 12 | /** 13 | * Represents the source of input data. 14 | */ 15 | public interface DataSource { 16 | 17 | void initialize(); 18 | 19 | void deinitialize(); 20 | 21 | boolean isInitialized(); 22 | 23 | /** 24 | * Metadata information. Returns the video orientation, or 0. 25 | * 26 | * @return video metadata orientation 27 | */ 28 | int getOrientation(); 29 | 30 | /** 31 | * Metadata information. Returns the video location, or null. 32 | * 33 | * @return video location or null 34 | */ 35 | @Nullable 36 | double[] getLocation(); 37 | 38 | /** 39 | * Returns the video total duration in microseconds. 40 | * 41 | * @return duration in us 42 | */ 43 | long getDurationUs(); 44 | 45 | /** 46 | * Called before starting to inspect the input format for this track. 47 | * Can return null if this media does not include this track type. 48 | * 49 | * @param type track type 50 | * @return format or null 51 | */ 52 | @Nullable 53 | MediaFormat getTrackFormat(@NonNull TrackType type); 54 | 55 | /** 56 | * Called before starting, but after {@link #getTrackFormat(TrackType)}, 57 | * to select the given track. 58 | * 59 | * @param type track type 60 | */ 61 | void selectTrack(@NonNull TrackType type); 62 | 63 | /** 64 | * Moves all selected tracks to the specified presentation time. 65 | * The timestamp should be between 0 and {@link #getDurationUs()}. 66 | * The actual timestamp might differ from the desired one because of 67 | * seeking constraints (e.g. seek to sync frames). It will typically be smaller 68 | * because we use {@link android.media.MediaExtractor#SEEK_TO_PREVIOUS_SYNC} in 69 | * the default source. 70 | * 71 | * @param desiredPositionUs requested timestamp 72 | * @return actual timestamp, likely smaller or equal 73 | */ 74 | long seekTo(long desiredPositionUs); 75 | 76 | /** 77 | * Returns true if we can read the given track at this point. 78 | * If true if returned, source should expect a {@link #readTrack(Chunk)} call. 79 | * 80 | * @param type track type 81 | * @return true if we can read this track now 82 | */ 83 | boolean canReadTrack(@NonNull TrackType type); 84 | 85 | /** 86 | * Called to read contents for the current track type. 87 | * Contents should be put inside {@link DataSource.Chunk#buffer}, and the 88 | * other chunk flags should be filled. 89 | * 90 | * @param chunk output chunk 91 | */ 92 | void readTrack(@NonNull DataSource.Chunk chunk); 93 | 94 | /** 95 | * Returns the current read position, between 0 and duration. 96 | * @return position in us 97 | */ 98 | long getPositionUs(); 99 | 100 | /** 101 | * When this source has been totally read, it can return true here to 102 | * notify an end of input stream. 103 | * 104 | * @return true if drained 105 | */ 106 | boolean isDrained(); 107 | 108 | /** 109 | * Called to release resources for a given track. 110 | * @param type track type 111 | */ 112 | void releaseTrack(@NonNull TrackType type); 113 | 114 | /** 115 | * Rewinds this source, moving it to its default state. 116 | * To be used again, tracks will be selected again. 117 | * After this call, for instance, 118 | * - {@link #getPositionUs()} should be 0 119 | * - {@link #isDrained()} should be false 120 | * - {@link #readTrack(Chunk)} should return the very first bytes 121 | */ 122 | // void rewind(); 123 | 124 | /** 125 | * Represents a chunk of data. 126 | * Can be used to read input from {@link #readTrack(Chunk)}. 127 | */ 128 | class Chunk { 129 | public ByteBuffer buffer; 130 | public boolean keyframe; 131 | public long timeUs; 132 | public boolean render; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/source/DataSourceWrapper.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.source; 2 | 3 | 4 | import android.media.MediaFormat; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.annotation.Nullable; 8 | 9 | import com.otaliastudios.transcoder.common.TrackType; 10 | 11 | /** 12 | * A {@link DataSource} wrapper that simply delegates all methods to the 13 | * wrapped source. It is the implementor responsibility to care about the case where 14 | * the wrapped source is already initialized, in case they are overriding initialize. 15 | */ 16 | public class DataSourceWrapper implements DataSource { 17 | 18 | private DataSource mSource; 19 | 20 | @SuppressWarnings("WeakerAccess") 21 | protected DataSourceWrapper(@NonNull DataSource source) { 22 | mSource = source; 23 | } 24 | 25 | // Only use if you know what you are doing 26 | protected DataSourceWrapper() { 27 | mSource = null; 28 | } 29 | 30 | @NonNull 31 | protected DataSource getSource() { 32 | return mSource; 33 | } 34 | 35 | // Only use if you know what you are doing 36 | protected void setSource(@NonNull DataSource source) { 37 | mSource = source; 38 | } 39 | 40 | @Override 41 | public int getOrientation() { 42 | return mSource.getOrientation(); 43 | } 44 | 45 | @Nullable 46 | @Override 47 | public double[] getLocation() { 48 | return mSource.getLocation(); 49 | } 50 | 51 | @Override 52 | public long getDurationUs() { 53 | return mSource.getDurationUs(); 54 | } 55 | 56 | @Nullable 57 | @Override 58 | public MediaFormat getTrackFormat(@NonNull TrackType type) { 59 | return mSource.getTrackFormat(type); 60 | } 61 | 62 | @Override 63 | public void selectTrack(@NonNull TrackType type) { 64 | mSource.selectTrack(type); 65 | } 66 | 67 | @Override 68 | public long seekTo(long desiredPositionUs) { 69 | return mSource.seekTo(desiredPositionUs); 70 | } 71 | 72 | @Override 73 | public boolean canReadTrack(@NonNull TrackType type) { 74 | return mSource.canReadTrack(type); 75 | } 76 | 77 | @Override 78 | public void readTrack(@NonNull Chunk chunk) { 79 | mSource.readTrack(chunk); 80 | } 81 | 82 | @Override 83 | public long getPositionUs() { 84 | return mSource.getPositionUs(); 85 | } 86 | 87 | @Override 88 | public boolean isDrained() { 89 | return mSource.isDrained(); 90 | } 91 | 92 | @Override 93 | public void releaseTrack(@NonNull TrackType type) { 94 | mSource.releaseTrack(type); 95 | } 96 | 97 | @Override 98 | public boolean isInitialized() { 99 | return mSource != null && mSource.isInitialized(); 100 | } 101 | 102 | @Override 103 | public void initialize() { 104 | // Make it easier for subclasses to put their logic in initialize safely. 105 | if (!isInitialized()) { 106 | if (mSource == null) { 107 | throw new NullPointerException("DataSourceWrapper's source is not set!"); 108 | } 109 | mSource.initialize(); 110 | } 111 | } 112 | 113 | @Override 114 | public void deinitialize() { 115 | mSource.deinitialize(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/source/FileDescriptorDataSource.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.source; 2 | 3 | import android.content.res.AssetFileDescriptor; 4 | import android.media.MediaExtractor; 5 | import android.media.MediaMetadataRetriever; 6 | 7 | import java.io.FileDescriptor; 8 | import java.io.IOException; 9 | 10 | import androidx.annotation.NonNull; 11 | 12 | /** 13 | * A {@link DataSource} backed by a file descriptor. 14 | * It is the caller responsibility to close the file descriptor. 15 | */ 16 | public class FileDescriptorDataSource extends DefaultDataSource { 17 | 18 | @NonNull 19 | private final FileDescriptor descriptor; 20 | private final long offset; 21 | private final long length; 22 | 23 | public FileDescriptorDataSource(@NonNull FileDescriptor descriptor) { 24 | // length is intentionally less than LONG_MAX, see retriever 25 | this(descriptor, 0, 0x7ffffffffffffffL); 26 | } 27 | 28 | public FileDescriptorDataSource(@NonNull FileDescriptor descriptor, long offset, long length) { 29 | this.descriptor = descriptor; 30 | this.offset = offset; 31 | this.length = length > 0 ? length : 0x7ffffffffffffffL; 32 | } 33 | 34 | @Override 35 | protected void initializeExtractor(@NonNull MediaExtractor extractor) throws IOException { 36 | extractor.setDataSource(descriptor, offset, length); 37 | } 38 | 39 | @Override 40 | protected void initializeRetriever(@NonNull MediaMetadataRetriever retriever) { 41 | retriever.setDataSource(descriptor, offset, length); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/source/FilePathDataSource.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.source; 2 | 3 | import android.media.MediaExtractor; 4 | 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | 8 | import androidx.annotation.NonNull; 9 | 10 | /** 11 | * A {@link DataSource} backed by a file absolute path. 12 | * 13 | * This class actually wraps a {@link FileDescriptorDataSource} for the apply() operations. 14 | * We could pass the path directly to MediaExtractor and MediaMetadataRetriever, but that is 15 | * discouraged since they could not be able to open the file from another process. 16 | * 17 | * See {@link MediaExtractor#setDataSource(String)} documentation. 18 | */ 19 | public class FilePathDataSource extends DataSourceWrapper { 20 | private FileInputStream mStream; 21 | private final String mPath; 22 | 23 | public FilePathDataSource(@NonNull String path) { 24 | mPath = path; 25 | } 26 | 27 | @Override 28 | public void initialize() { 29 | try { 30 | mStream = new FileInputStream(mPath); 31 | setSource(new FileDescriptorDataSource(mStream.getFD())); 32 | } catch (IOException e) { 33 | throw new RuntimeException(e); 34 | } 35 | super.initialize(); 36 | } 37 | 38 | @Override 39 | public void deinitialize() { 40 | try { mStream.close(); } catch (IOException ignore) { } 41 | super.deinitialize(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/source/UriDataSource.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.source; 2 | 3 | import android.content.Context; 4 | import android.media.MediaExtractor; 5 | import android.media.MediaMetadataRetriever; 6 | import android.net.Uri; 7 | 8 | import java.io.IOException; 9 | 10 | import androidx.annotation.NonNull; 11 | 12 | /** 13 | * A {@link DataSource} backed by an Uri, possibly 14 | * a content:// uri. 15 | */ 16 | public class UriDataSource extends DefaultDataSource { 17 | 18 | @NonNull private final Context context; 19 | @NonNull private final Uri uri; 20 | 21 | public UriDataSource(@NonNull Context context, @NonNull Uri uri) { 22 | this.context = context.getApplicationContext(); 23 | this.uri = uri; 24 | } 25 | 26 | @Override 27 | protected void initializeExtractor(@NonNull MediaExtractor extractor) throws IOException { 28 | extractor.setDataSource(context, uri, null); 29 | } 30 | 31 | @Override 32 | protected void initializeRetriever(@NonNull MediaMetadataRetriever retriever) { 33 | retriever.setDataSource(context, uri); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/strategy/DefaultVideoStrategies.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.strategy; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | /** 6 | * Contains presets and utilities for defining a {@link DefaultVideoStrategy}. 7 | */ 8 | public class DefaultVideoStrategies { 9 | 10 | private DefaultVideoStrategies() {} 11 | 12 | /** 13 | * A {@link DefaultVideoStrategy} that uses 720x1280. 14 | * This preset is ensured to work on any Android >=4.3 devices by Android CTS, 15 | * assuming that the codec is available. 16 | * 17 | * @return a default video strategy 18 | */ 19 | @NonNull 20 | public static DefaultVideoStrategy for720x1280() { 21 | return DefaultVideoStrategy.exact(720, 1280) 22 | .bitRate(2L * 1000 * 1000) 23 | .frameRate(30) 24 | .keyFrameInterval(3F) 25 | .build(); 26 | } 27 | 28 | /** 29 | * A {@link DefaultVideoStrategy} that uses 360x480 (3:4), 30 | * ensured to work for 3:4 videos as explained by 31 | * https://developer.android.com/guide/topics/media/media-formats 32 | * 33 | * @return a default video strategy 34 | */ 35 | @SuppressWarnings("unused") 36 | @NonNull 37 | public static DefaultVideoStrategy for360x480() { 38 | return DefaultVideoStrategy.exact(360, 480) 39 | .bitRate(500L * 1000) 40 | .frameRate(30) 41 | .keyFrameInterval(3F) 42 | .build(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/strategy/PassThroughTrackStrategy.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.strategy; 2 | 3 | import android.media.MediaFormat; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import com.otaliastudios.transcoder.common.TrackStatus; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * An {@link TrackStrategy} that asks the encoder to keep this track as is. 13 | * Note that this is risky, as the track type might not be supported by 14 | * the mp4 container. 15 | */ 16 | @SuppressWarnings("unused") 17 | public class PassThroughTrackStrategy implements TrackStrategy { 18 | 19 | @NonNull 20 | @Override 21 | public TrackStatus createOutputFormat(@NonNull List inputFormats, @NonNull MediaFormat outputFormat) { 22 | return TrackStatus.PASS_THROUGH; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/strategy/RemoveTrackStrategy.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.strategy; 2 | 3 | import android.media.MediaFormat; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import com.otaliastudios.transcoder.common.TrackStatus; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * An {@link TrackStrategy} that removes this track from output. 13 | */ 14 | @SuppressWarnings("unused") 15 | public class RemoveTrackStrategy implements TrackStrategy { 16 | 17 | @NonNull 18 | @Override 19 | public TrackStatus createOutputFormat(@NonNull List inputFormats, @NonNull MediaFormat outputFormat) { 20 | return TrackStatus.REMOVING; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/strategy/TrackStrategy.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.strategy; 2 | 3 | import android.media.MediaFormat; 4 | 5 | import com.otaliastudios.transcoder.common.TrackStatus; 6 | import com.otaliastudios.transcoder.resize.Resizer; 7 | 8 | import androidx.annotation.NonNull; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * Base class for video/audio format strategy. 14 | * Video strategies should use a {@link Resizer} instance to compute the output 15 | * video size. 16 | */ 17 | public interface TrackStrategy { 18 | 19 | /** 20 | * Create the output format for this track (either audio or video). 21 | * Implementors should fill the outputFormat object and return a non-null {@link TrackStatus}: 22 | * - {@link TrackStatus#COMPRESSING}: we want to compress this track. Output format will be used 23 | * - {@link TrackStatus#PASS_THROUGH}: we want to use the input format. Output format will be ignored 24 | * - {@link TrackStatus#REMOVING}: we want to remove this track. Output format will be ignored 25 | * 26 | * Subclasses can also throw to abort the whole transcoding operation. 27 | * 28 | * @param inputFormats the input formats 29 | * @param outputFormat the output format to be filled 30 | * @return the track status 31 | */ 32 | @NonNull 33 | TrackStatus createOutputFormat(@NonNull List inputFormats, @NonNull MediaFormat outputFormat); 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/stretch/AudioStretcher.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.stretch; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.nio.Buffer; 6 | import java.nio.ShortBuffer; 7 | import java.util.Random; 8 | 9 | /** 10 | * An AudioStretcher will change audio samples duration, in response to a 11 | * {@link com.otaliastudios.transcoder.time.TimeInterpolator} that altered the sample timestamp. 12 | * 13 | * This can mean either shrink the sample (in case of video speed up) or elongate it (in case of 14 | * video slow down) so that it matches the output size. 15 | */ 16 | public interface AudioStretcher { 17 | 18 | /** 19 | * Stretches the input into the output, based on the {@link Buffer#remaining()} value of both. 20 | * At the end of this method, the {@link Buffer#position()} of both should be equal to their 21 | * respective {@link Buffer#limit()}. 22 | * 23 | * And of course, both {@link Buffer#limit()}s should remain unchanged. 24 | * 25 | * @param input input buffer 26 | * @param output output buffer 27 | * @param channels audio channels 28 | */ 29 | void stretch(@NonNull ShortBuffer input, @NonNull ShortBuffer output, int channels); 30 | 31 | AudioStretcher PASSTHROUGH = new PassThroughAudioStretcher(); 32 | 33 | AudioStretcher CUT = new CutAudioStretcher(); 34 | 35 | AudioStretcher INSERT = new InsertAudioStretcher(); 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/stretch/CutAudioStretcher.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.stretch; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.nio.ShortBuffer; 6 | 7 | /** 8 | * A {@link AudioStretcher} meant to be used when output size is smaller than the input. 9 | * Cutting the latest samples is a way to go that does not modify the audio pitch. 10 | */ 11 | public class CutAudioStretcher implements AudioStretcher { 12 | 13 | @Override 14 | public void stretch(@NonNull ShortBuffer input, @NonNull ShortBuffer output, int channels) { 15 | if (input.remaining() < output.remaining()) { 16 | throw new IllegalArgumentException("Illegal use of CutAudioStretcher"); 17 | } 18 | int exceeding = input.remaining() - output.remaining(); 19 | input.limit(input.limit() - exceeding); // Make remaining() the same for both 20 | output.put(input); // Safely bulk-put 21 | input.limit(input.limit() + exceeding); // Restore 22 | input.position(input.limit()); // Make as if we have read it all 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/stretch/DefaultAudioStretcher.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.stretch; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.nio.ShortBuffer; 6 | 7 | /** 8 | * An {@link AudioStretcher} that delegates to appropriate classes 9 | * based on input and output size. 10 | */ 11 | public class DefaultAudioStretcher implements AudioStretcher { 12 | 13 | @Override 14 | public void stretch(@NonNull ShortBuffer input, @NonNull ShortBuffer output, int channels) { 15 | if (input.remaining() < output.remaining()) { 16 | INSERT.stretch(input, output, channels); 17 | } else if (input.remaining() > output.remaining()) { 18 | CUT.stretch(input, output, channels); 19 | } else { 20 | PASSTHROUGH.stretch(input, output, channels); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/stretch/InsertAudioStretcher.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.stretch; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.nio.ShortBuffer; 6 | import java.util.Random; 7 | 8 | /** 9 | * A {@link AudioStretcher} meant to be used when output size is bigger than the input. 10 | * It will insert noise samples to fill the gaps, at regular intervals. 11 | * This modifies the audio pitch of course. 12 | */ 13 | public class InsertAudioStretcher implements AudioStretcher { 14 | 15 | private final static Random NOISE = new Random(); 16 | 17 | private static short noise() { 18 | return (short) NOISE.nextInt(300); 19 | } 20 | 21 | private static float ratio(int remaining, int all) { 22 | return (float) remaining / all; 23 | } 24 | 25 | @Override 26 | public void stretch(@NonNull ShortBuffer input, @NonNull ShortBuffer output, int channels) { 27 | if (input.remaining() >= output.remaining()) { 28 | throw new IllegalArgumentException("Illegal use of AudioStretcher.INSERT"); 29 | } 30 | if (channels != 1 && channels != 2) { 31 | throw new IllegalArgumentException("Illegal use of AudioStretcher.INSERT. Channels:" + channels); 32 | } 33 | final int inputSamples = input.remaining() / channels; 34 | final int fakeSamples = (int) Math.floor((double) (output.remaining() - input.remaining()) / channels); 35 | int remainingInputSamples = inputSamples; 36 | int remainingFakeSamples = fakeSamples; 37 | float remainingInputSamplesRatio = ratio(remainingInputSamples, inputSamples); 38 | float remainingFakeSamplesRatio = ratio(remainingFakeSamples, fakeSamples); 39 | while (remainingInputSamples > 0 && remainingFakeSamples > 0) { 40 | // Will this be an input sample or a fake sample? 41 | // Choose the one with the bigger ratio. 42 | if (remainingInputSamplesRatio >= remainingFakeSamplesRatio) { 43 | output.put(input.get()); 44 | if (channels == 2) output.put(input.get()); 45 | remainingInputSamples--; 46 | remainingInputSamplesRatio = ratio(remainingInputSamples, inputSamples); 47 | } else { 48 | output.put(noise()); 49 | if (channels == 2) output.put(noise()); 50 | remainingFakeSamples--; 51 | remainingFakeSamplesRatio = ratio(remainingFakeSamples, inputSamples); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/stretch/PassThroughAudioStretcher.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.stretch; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.nio.ShortBuffer; 6 | 7 | /** 8 | * A no-op {@link AudioStretcher} that copies input into output. 9 | */ 10 | public class PassThroughAudioStretcher implements AudioStretcher { 11 | 12 | @Override 13 | public void stretch(@NonNull ShortBuffer input, @NonNull ShortBuffer output, int channels) { 14 | if (input.remaining() > output.remaining()) { 15 | throw new IllegalArgumentException("Illegal use of PassThroughAudioStretcher"); 16 | } 17 | output.put(input); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/thumbnail/CoverThumbnailRequest.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.thumbnail 2 | 3 | class CoverThumbnailRequest : ThumbnailRequest { 4 | override fun locate(durationUs: Long) = listOf(0L) 5 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/thumbnail/SingleThumbnailRequest.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.thumbnail 2 | 3 | class SingleThumbnailRequest(private val positionUs: Long) : ThumbnailRequest { 4 | override fun locate(durationUs: Long): List { 5 | require(positionUs in 0L..durationUs) { 6 | "Thumbnail position is out of range. position=$positionUs range=${0L..durationUs}" 7 | } 8 | return listOf(positionUs) 9 | } 10 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/thumbnail/Thumbnail.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.thumbnail 2 | 3 | import android.graphics.Bitmap 4 | 5 | class Thumbnail internal constructor( 6 | val request: ThumbnailRequest, 7 | val positionUs: Long, 8 | val bitmap: Bitmap 9 | ) -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/thumbnail/ThumbnailRequest.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.thumbnail 2 | 3 | interface ThumbnailRequest { 4 | fun locate(durationUs: Long): List 5 | 6 | // Could make it so that if locate() is empty, accept is called for each frame (no seeking). 7 | // But this only makes sense if accept signature has more information (segment, ...), and 8 | // it should also have a way to say - we're done, stop transcoding. 9 | // fun accept(positionUs: Long): Boolean 10 | 11 | // Could add resizing per request 12 | // val resizer = PassThroughResizer() 13 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/thumbnail/UniformThumbnailRequest.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.thumbnail 2 | 3 | class UniformThumbnailRequest(private val count: Int) : ThumbnailRequest { 4 | 5 | init { 6 | require(count >= 2) { 7 | "At least 2 thumbnails should be requested when using UniformThumbnailRequest." 8 | } 9 | } 10 | 11 | override fun locate(durationUs: Long): List { 12 | val list = mutableListOf() 13 | var positionUs = 0L 14 | val stepUs = durationUs / (count - 1) 15 | repeat(count) { 16 | list.add(positionUs) 17 | positionUs += stepUs 18 | } 19 | return list 20 | } 21 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/time/DefaultTimeInterpolator.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.time; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.TrackType; 6 | 7 | /** 8 | * A {@link TimeInterpolator} that does no time interpolation or correction - 9 | * it just returns the input time. 10 | */ 11 | public class DefaultTimeInterpolator implements TimeInterpolator { 12 | 13 | @Override 14 | public long interpolate(@NonNull TrackType type, long time) { 15 | return time; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/time/MonotonicTimeInterpolator.kt: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.time 2 | 3 | import android.media.MediaMuxer 4 | import com.otaliastudios.transcoder.common.TrackType 5 | import com.otaliastudios.transcoder.internal.utils.mutableTrackMapOf 6 | 7 | /** 8 | * A [TimeInterpolator] that ensures timestamps are monotonically increasing. 9 | * Timestamps can go back and forth for many reasons, like miscalculations in MediaCodec output 10 | * or manually generated timestamps, or at the boundary between one data source and another. 11 | * 12 | * Since [MediaMuxer.writeSampleData] can throw in case of invalid timestamps, this interpolator 13 | * ensures that the next timestamp is at least equal to the previous timestamp plus 1. 14 | * It does no effort to preserve the input deltas, so the input stream must be as consistent as possible. 15 | * 16 | * For example, 20 30 40 50 10 20 30 would become 20 30 40 50 51 52 53. 17 | */ 18 | internal class MonotonicTimeInterpolator : TimeInterpolator { 19 | private val last = mutableTrackMapOf(Long.MIN_VALUE, Long.MIN_VALUE) 20 | override fun interpolate(type: TrackType, time: Long): Long { 21 | return interpolate(last[type], time).also { last[type] = it } 22 | } 23 | private fun interpolate(prev: Long, next: Long): Long { 24 | if (prev == Long.MIN_VALUE) return next 25 | return next.coerceAtLeast(prev + 1) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/time/SpeedTimeInterpolator.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.time; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.TrackType; 6 | import com.otaliastudios.transcoder.internal.utils.TrackMap; 7 | import com.otaliastudios.transcoder.internal.utils.Logger; 8 | 9 | import static com.otaliastudios.transcoder.internal.utils.TrackMapKt.trackMapOf; 10 | 11 | 12 | /** 13 | * A {@link TimeInterpolator} that modifies the playback speed by the given 14 | * float factor. A factor less than 1 will slow down, while a bigger factor will 15 | * accelerate. 16 | */ 17 | public class SpeedTimeInterpolator implements TimeInterpolator { 18 | 19 | private final static Logger LOG = new Logger("SpeedTimeInterpolator"); 20 | 21 | private final double mFactor; 22 | private final TrackMap mTrackData = trackMapOf(new TrackData(), new TrackData()); 23 | 24 | /** 25 | * Creates a new speed interpolator for the given factor. 26 | * Throws if factor is less than 0 or equal to 0. 27 | * @param factor a factor 28 | */ 29 | public SpeedTimeInterpolator(float factor) { 30 | if (factor <= 0) { 31 | throw new IllegalArgumentException("Invalid speed factor: " + factor); 32 | } 33 | mFactor = factor; 34 | } 35 | 36 | /** 37 | * Returns the factor passed to the constructor. 38 | * @return the factor 39 | */ 40 | @SuppressWarnings("unused") 41 | public float getFactor(@NonNull TrackType type, long time) { 42 | return (float) mFactor; 43 | } 44 | 45 | @Override 46 | public long interpolate(@NonNull TrackType type, long time) { 47 | TrackData data = mTrackData.get(type); 48 | if (data.lastRealTime == Long.MIN_VALUE) { 49 | data.lastRealTime = time; 50 | data.lastCorrectedTime = time; 51 | } else { 52 | long realDelta = time - data.lastRealTime; 53 | long correctedDelta = (long) ((double) realDelta / getFactor(type, time)); 54 | data.lastRealTime = time; 55 | data.lastCorrectedTime += correctedDelta; 56 | } 57 | LOG.v("Track:" + type + " inputTime:" + time + " outputTime:" + data.lastCorrectedTime); 58 | return data.lastCorrectedTime; 59 | } 60 | 61 | private static class TrackData { 62 | private long lastRealTime = Long.MIN_VALUE; 63 | private long lastCorrectedTime = Long.MIN_VALUE; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/time/TimeInterpolator.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.time; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.TrackType; 6 | 7 | /** 8 | * An interface to redefine the time between video or audio frames. 9 | */ 10 | public interface TimeInterpolator { 11 | 12 | /** 13 | * Given the track type (audio or video) and the frame timestamp in microseconds, 14 | * should return the corrected timestamp. 15 | * 16 | * @param type track type 17 | * @param time frame timestamp in microseconds 18 | * @return the new frame timestamp 19 | */ 20 | long interpolate(@NonNull TrackType type, long time); 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/validator/DefaultValidator.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.validator; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.TrackStatus; 6 | 7 | /** 8 | * The default {@link Validator} to understand whether to keep going with the 9 | * transcoding process or to abort and notify the listener. 10 | */ 11 | public class DefaultValidator implements Validator { 12 | 13 | @Override 14 | public boolean validate(@NonNull TrackStatus videoStatus, @NonNull TrackStatus audioStatus) { 15 | if (videoStatus == TrackStatus.COMPRESSING || audioStatus == TrackStatus.COMPRESSING) { 16 | // If someone is compressing, keep going. 17 | return true; 18 | } 19 | // Both tracks are either absent, passthrough or being removed. Would be tempted 20 | // to return false here, however a removal might be a intentional action: Keep going. 21 | // noinspection RedundantIfStatement 22 | if (videoStatus == TrackStatus.REMOVING || audioStatus == TrackStatus.REMOVING) { 23 | return true; 24 | } 25 | 26 | // At this point it's either ABSENT or PASS_THROUGH so we are safe aborting 27 | // the process. 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/validator/Validator.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.validator; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.TrackStatus; 6 | import com.otaliastudios.transcoder.strategy.TrackStrategy; 7 | 8 | /** 9 | * A validator determines if the transcoding process should proceed or not, 10 | * after the {@link TrackStrategy} have 11 | * provided the output format. 12 | */ 13 | public interface Validator { 14 | 15 | /** 16 | * Return true if the transcoding should proceed, false otherwise. 17 | * 18 | * @param videoStatus the status of the video track 19 | * @param audioStatus the status of the audio track 20 | * @return true to proceed 21 | */ 22 | boolean validate(@NonNull TrackStatus videoStatus, @NonNull TrackStatus audioStatus); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/validator/WriteAlwaysValidator.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.validator; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.TrackStatus; 6 | 7 | /** 8 | * A {@link Validator} that always writes to target file, no matter the track status, 9 | * presence of tracks and so on. The output container file might be empty or unnecessary. 10 | */ 11 | @SuppressWarnings("unused") 12 | public class WriteAlwaysValidator implements Validator { 13 | 14 | @Override 15 | public boolean validate(@NonNull TrackStatus videoStatus, @NonNull TrackStatus audioStatus) { 16 | return true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/main/java/com/otaliastudios/transcoder/validator/WriteVideoValidator.java: -------------------------------------------------------------------------------- 1 | package com.otaliastudios.transcoder.validator; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.otaliastudios.transcoder.common.TrackStatus; 6 | 7 | /** 8 | * A {@link Validator} that gives priority to the video track. 9 | * Transcoding will not happen if the video track does not need it, even if the 10 | * audio track might need it. 11 | */ 12 | @SuppressWarnings("unused") 13 | public class WriteVideoValidator implements Validator { 14 | 15 | @Override 16 | public boolean validate(@NonNull TrackStatus videoStatus, @NonNull TrackStatus audioStatus) { 17 | switch (videoStatus) { 18 | case ABSENT: return false; 19 | case REMOVING: return true; 20 | case COMPRESSING: return true; 21 | case PASS_THROUGH: return false; 22 | } 23 | return true; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenLocal() 4 | google() 5 | gradlePluginPortal() 6 | mavenCentral() 7 | } 8 | } 9 | 10 | dependencyResolutionManagement { 11 | repositories { 12 | mavenCentral() 13 | google() 14 | } 15 | } 16 | 17 | include(":lib") 18 | include(":lib-legacy") 19 | include(":demo") 20 | --------------------------------------------------------------------------------