From 74635ece92ab86267d46b273e54c90e9aee58fc0 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 3 Aug 2024 15:00:58 +0200 Subject: [PATCH 01/14] Docs: redirect to new website --- docs/_about/changelog.md | 3 ++- docs/_about/getting-started.md | 3 ++- docs/_about/install.md | 3 ++- docs/_docs/advanced-options.md | 3 ++- docs/_docs/clipping.md | 3 ++- docs/_docs/concatenation.md | 3 ++- docs/_docs/data-sources.md | 3 ++- docs/_docs/events.md | 3 ++- docs/_docs/track-strategies.md | 3 ++- docs/_docs/validators.md | 3 ++- docs/_extra/contact.md | 3 ++- docs/_extra/contributing.md | 3 ++- docs/_extra/donate.md | 3 ++- docs/_layouts/redirect.html | 15 +++++++++++++++ docs/home.md | 3 ++- docs/index.md | 3 ++- 16 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 docs/_layouts/redirect.html diff --git a/docs/_about/changelog.md b/docs/_about/changelog.md index 006ae4b3..283772c7 100644 --- a/docs/_about/changelog.md +++ b/docs/_about/changelog.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder/changelog title: "Changelog" order: 3 --- diff --git a/docs/_about/getting-started.md b/docs/_about/getting-started.md index 826130d9..11b8f971 100644 --- a/docs/_about/getting-started.md +++ b/docs/_about/getting-started.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder/install title: "Getting Started" description: "Simple guide to transcode your first video" order: 2 diff --git a/docs/_about/install.md b/docs/_about/install.md index 177524bd..87fc459b 100644 --- a/docs/_about/install.md +++ b/docs/_about/install.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder/install title: "Install" description: "Integrate in your project" order: 1 diff --git a/docs/_docs/advanced-options.md b/docs/_docs/advanced-options.md index 0902d23b..2a117862 100644 --- a/docs/_docs/advanced-options.md +++ b/docs/_docs/advanced-options.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder/advanced-options title: "Advanced Options" description: "Advanced transcoding options" order: 7 diff --git a/docs/_docs/clipping.md b/docs/_docs/clipping.md index 783129d1..f7d13752 100644 --- a/docs/_docs/clipping.md +++ b/docs/_docs/clipping.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder/clipping title: "Clipping and trimming" description: "How to clip each segment individually on both ends" order: 2 diff --git a/docs/_docs/concatenation.md b/docs/_docs/concatenation.md index d311e90a..421e0d59 100644 --- a/docs/_docs/concatenation.md +++ b/docs/_docs/concatenation.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder/concatenation title: "Concatenation" description: "How to concatenate video segments" order: 3 diff --git a/docs/_docs/data-sources.md b/docs/_docs/data-sources.md index ee45591a..6b5d935c 100644 --- a/docs/_docs/data-sources.md +++ b/docs/_docs/data-sources.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder/data-sources title: "Data Sources" description: "Sources of media data" order: 1 diff --git a/docs/_docs/events.md b/docs/_docs/events.md index a2f79b9f..e66974b2 100644 --- a/docs/_docs/events.md +++ b/docs/_docs/events.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder/events title: "Transcoding Events" description: "Listening to transcoding events" order: 4 diff --git a/docs/_docs/track-strategies.md b/docs/_docs/track-strategies.md index b78fbeb0..00b9ab9b 100644 --- a/docs/_docs/track-strategies.md +++ b/docs/_docs/track-strategies.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder/track-strategies title: "Track Strategies" description: "Per-track transcoding options" order: 6 diff --git a/docs/_docs/validators.md b/docs/_docs/validators.md index f28eb3a0..1692554e 100644 --- a/docs/_docs/validators.md +++ b/docs/_docs/validators.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder/validators title: "Validators" description: "Validate or abort the transcoding process" order: 5 diff --git a/docs/_extra/contact.md b/docs/_extra/contact.md index 409dfe15..6aa9334f 100644 --- a/docs/_extra/contact.md +++ b/docs/_extra/contact.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder title: "Contact" order: 3 --- diff --git a/docs/_extra/contributing.md b/docs/_extra/contributing.md index cf62752b..307a4f98 100644 --- a/docs/_extra/contributing.md +++ b/docs/_extra/contributing.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder title: "Contributing & License" order: 1 --- diff --git a/docs/_extra/donate.md b/docs/_extra/donate.md index 3400e886..68d25066 100644 --- a/docs/_extra/donate.md +++ b/docs/_extra/donate.md @@ -1,5 +1,6 @@ --- -layout: page +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder title: "Donate" order: 2 --- diff --git a/docs/_layouts/redirect.html b/docs/_layouts/redirect.html new file mode 100644 index 00000000..f057557a --- /dev/null +++ b/docs/_layouts/redirect.html @@ -0,0 +1,15 @@ + + + + + Redirecting… + + + + + + +

Redirecting…

+Click here if you are not redirected. + + \ No newline at end of file diff --git a/docs/home.md b/docs/home.md index d5ad2786..9b95afd5 100644 --- a/docs/home.md +++ b/docs/home.md @@ -1,5 +1,6 @@ --- -layout: main +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder title: "Transcoder" --- diff --git a/docs/index.md b/docs/index.md index ecfd1b98..82d0f3bf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,6 @@ --- -layout: landing +layout: redirect +redirect_to: https://opensource.deepmedia.io/transcoder title: "Transcoder" --- From 96295f3ea3d88235c2075b7a7ad8d39db5bda7c9 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 3 Aug 2024 15:01:38 +0200 Subject: [PATCH 02/14] Docs: rename docs -> docs-legacy --- {docs => docs-legacy}/.gitignore | 0 {docs => docs-legacy}/Gemfile | 0 {docs => docs-legacy}/README.md | 0 {docs => docs-legacy}/_about/changelog.md | 0 {docs => docs-legacy}/_about/getting-started.md | 0 {docs => docs-legacy}/_about/install.md | 0 {docs => docs-legacy}/_config.yml | 0 {docs => docs-legacy}/_docs/advanced-options.md | 0 {docs => docs-legacy}/_docs/clipping.md | 0 {docs => docs-legacy}/_docs/concatenation.md | 0 {docs => docs-legacy}/_docs/data-sources.md | 0 {docs => docs-legacy}/_docs/events.md | 0 {docs => docs-legacy}/_docs/track-strategies.md | 0 {docs => docs-legacy}/_docs/validators.md | 0 {docs => docs-legacy}/_extra/contact.md | 0 {docs => docs-legacy}/_extra/contributing.md | 0 {docs => docs-legacy}/_extra/donate.md | 0 {docs => docs-legacy}/_includes/disqus.html | 0 {docs => docs-legacy}/_includes/footer.html | 0 .../_includes/google_analytics.html | 0 {docs => docs-legacy}/_includes/head.html | 0 {docs => docs-legacy}/_includes/header.html | 0 {docs => docs-legacy}/_includes/navigation.html | 0 {docs => docs-legacy}/_layouts/landing.html | 0 {docs => docs-legacy}/_layouts/main.html | 0 {docs => docs-legacy}/_layouts/page.html | 0 {docs => docs-legacy}/_layouts/redirect.html | 0 {docs => docs-legacy}/css/colors.css | 0 {docs => docs-legacy}/css/fonts.css | 0 {docs => docs-legacy}/css/fonts_responsive.css | 0 {docs => docs-legacy}/css/landing.css | 0 {docs => docs-legacy}/css/main.css | 0 {docs => docs-legacy}/css/syntax.css | 0 {docs => docs-legacy}/home.md | 0 {docs => docs-legacy}/icons/github.svg | 0 {docs => docs-legacy}/icons/menu.svg | 0 {docs => docs-legacy}/index.md | 0 {docs => docs-legacy}/script/launch | 0 {docs => docs-legacy}/static/banner.png | Bin {docs => docs-legacy}/static/icon_foreground.png | Bin {docs => docs-legacy}/static/screenshot-1.png | Bin {docs => docs-legacy}/static/screenshot-2.png | Bin {docs => docs-legacy}/static/sharechat.png | Bin 43 files changed, 0 insertions(+), 0 deletions(-) rename {docs => docs-legacy}/.gitignore (100%) rename {docs => docs-legacy}/Gemfile (100%) rename {docs => docs-legacy}/README.md (100%) rename {docs => docs-legacy}/_about/changelog.md (100%) rename {docs => docs-legacy}/_about/getting-started.md (100%) rename {docs => docs-legacy}/_about/install.md (100%) rename {docs => docs-legacy}/_config.yml (100%) rename {docs => docs-legacy}/_docs/advanced-options.md (100%) rename {docs => docs-legacy}/_docs/clipping.md (100%) rename {docs => docs-legacy}/_docs/concatenation.md (100%) rename {docs => docs-legacy}/_docs/data-sources.md (100%) rename {docs => docs-legacy}/_docs/events.md (100%) rename {docs => docs-legacy}/_docs/track-strategies.md (100%) rename {docs => docs-legacy}/_docs/validators.md (100%) rename {docs => docs-legacy}/_extra/contact.md (100%) rename {docs => docs-legacy}/_extra/contributing.md (100%) rename {docs => docs-legacy}/_extra/donate.md (100%) rename {docs => docs-legacy}/_includes/disqus.html (100%) rename {docs => docs-legacy}/_includes/footer.html (100%) rename {docs => docs-legacy}/_includes/google_analytics.html (100%) rename {docs => docs-legacy}/_includes/head.html (100%) rename {docs => docs-legacy}/_includes/header.html (100%) rename {docs => docs-legacy}/_includes/navigation.html (100%) rename {docs => docs-legacy}/_layouts/landing.html (100%) rename {docs => docs-legacy}/_layouts/main.html (100%) rename {docs => docs-legacy}/_layouts/page.html (100%) rename {docs => docs-legacy}/_layouts/redirect.html (100%) rename {docs => docs-legacy}/css/colors.css (100%) rename {docs => docs-legacy}/css/fonts.css (100%) rename {docs => docs-legacy}/css/fonts_responsive.css (100%) rename {docs => docs-legacy}/css/landing.css (100%) rename {docs => docs-legacy}/css/main.css (100%) rename {docs => docs-legacy}/css/syntax.css (100%) rename {docs => docs-legacy}/home.md (100%) rename {docs => docs-legacy}/icons/github.svg (100%) rename {docs => docs-legacy}/icons/menu.svg (100%) rename {docs => docs-legacy}/index.md (100%) rename {docs => docs-legacy}/script/launch (100%) rename {docs => docs-legacy}/static/banner.png (100%) rename {docs => docs-legacy}/static/icon_foreground.png (100%) rename {docs => docs-legacy}/static/screenshot-1.png (100%) rename {docs => docs-legacy}/static/screenshot-2.png (100%) rename {docs => docs-legacy}/static/sharechat.png (100%) diff --git a/docs/.gitignore b/docs-legacy/.gitignore similarity index 100% rename from docs/.gitignore rename to docs-legacy/.gitignore diff --git a/docs/Gemfile b/docs-legacy/Gemfile similarity index 100% rename from docs/Gemfile rename to docs-legacy/Gemfile diff --git a/docs/README.md b/docs-legacy/README.md similarity index 100% rename from docs/README.md rename to docs-legacy/README.md diff --git a/docs/_about/changelog.md b/docs-legacy/_about/changelog.md similarity index 100% rename from docs/_about/changelog.md rename to docs-legacy/_about/changelog.md diff --git a/docs/_about/getting-started.md b/docs-legacy/_about/getting-started.md similarity index 100% rename from docs/_about/getting-started.md rename to docs-legacy/_about/getting-started.md diff --git a/docs/_about/install.md b/docs-legacy/_about/install.md similarity index 100% rename from docs/_about/install.md rename to docs-legacy/_about/install.md diff --git a/docs/_config.yml b/docs-legacy/_config.yml similarity index 100% rename from docs/_config.yml rename to docs-legacy/_config.yml diff --git a/docs/_docs/advanced-options.md b/docs-legacy/_docs/advanced-options.md similarity index 100% rename from docs/_docs/advanced-options.md rename to docs-legacy/_docs/advanced-options.md diff --git a/docs/_docs/clipping.md b/docs-legacy/_docs/clipping.md similarity index 100% rename from docs/_docs/clipping.md rename to docs-legacy/_docs/clipping.md diff --git a/docs/_docs/concatenation.md b/docs-legacy/_docs/concatenation.md similarity index 100% rename from docs/_docs/concatenation.md rename to docs-legacy/_docs/concatenation.md diff --git a/docs/_docs/data-sources.md b/docs-legacy/_docs/data-sources.md similarity index 100% rename from docs/_docs/data-sources.md rename to docs-legacy/_docs/data-sources.md diff --git a/docs/_docs/events.md b/docs-legacy/_docs/events.md similarity index 100% rename from docs/_docs/events.md rename to docs-legacy/_docs/events.md diff --git a/docs/_docs/track-strategies.md b/docs-legacy/_docs/track-strategies.md similarity index 100% rename from docs/_docs/track-strategies.md rename to docs-legacy/_docs/track-strategies.md diff --git a/docs/_docs/validators.md b/docs-legacy/_docs/validators.md similarity index 100% rename from docs/_docs/validators.md rename to docs-legacy/_docs/validators.md diff --git a/docs/_extra/contact.md b/docs-legacy/_extra/contact.md similarity index 100% rename from docs/_extra/contact.md rename to docs-legacy/_extra/contact.md diff --git a/docs/_extra/contributing.md b/docs-legacy/_extra/contributing.md similarity index 100% rename from docs/_extra/contributing.md rename to docs-legacy/_extra/contributing.md diff --git a/docs/_extra/donate.md b/docs-legacy/_extra/donate.md similarity index 100% rename from docs/_extra/donate.md rename to docs-legacy/_extra/donate.md diff --git a/docs/_includes/disqus.html b/docs-legacy/_includes/disqus.html similarity index 100% rename from docs/_includes/disqus.html rename to docs-legacy/_includes/disqus.html diff --git a/docs/_includes/footer.html b/docs-legacy/_includes/footer.html similarity index 100% rename from docs/_includes/footer.html rename to docs-legacy/_includes/footer.html diff --git a/docs/_includes/google_analytics.html b/docs-legacy/_includes/google_analytics.html similarity index 100% rename from docs/_includes/google_analytics.html rename to docs-legacy/_includes/google_analytics.html diff --git a/docs/_includes/head.html b/docs-legacy/_includes/head.html similarity index 100% rename from docs/_includes/head.html rename to docs-legacy/_includes/head.html diff --git a/docs/_includes/header.html b/docs-legacy/_includes/header.html similarity index 100% rename from docs/_includes/header.html rename to docs-legacy/_includes/header.html diff --git a/docs/_includes/navigation.html b/docs-legacy/_includes/navigation.html similarity index 100% rename from docs/_includes/navigation.html rename to docs-legacy/_includes/navigation.html diff --git a/docs/_layouts/landing.html b/docs-legacy/_layouts/landing.html similarity index 100% rename from docs/_layouts/landing.html rename to docs-legacy/_layouts/landing.html diff --git a/docs/_layouts/main.html b/docs-legacy/_layouts/main.html similarity index 100% rename from docs/_layouts/main.html rename to docs-legacy/_layouts/main.html diff --git a/docs/_layouts/page.html b/docs-legacy/_layouts/page.html similarity index 100% rename from docs/_layouts/page.html rename to docs-legacy/_layouts/page.html diff --git a/docs/_layouts/redirect.html b/docs-legacy/_layouts/redirect.html similarity index 100% rename from docs/_layouts/redirect.html rename to docs-legacy/_layouts/redirect.html diff --git a/docs/css/colors.css b/docs-legacy/css/colors.css similarity index 100% rename from docs/css/colors.css rename to docs-legacy/css/colors.css diff --git a/docs/css/fonts.css b/docs-legacy/css/fonts.css similarity index 100% rename from docs/css/fonts.css rename to docs-legacy/css/fonts.css diff --git a/docs/css/fonts_responsive.css b/docs-legacy/css/fonts_responsive.css similarity index 100% rename from docs/css/fonts_responsive.css rename to docs-legacy/css/fonts_responsive.css diff --git a/docs/css/landing.css b/docs-legacy/css/landing.css similarity index 100% rename from docs/css/landing.css rename to docs-legacy/css/landing.css diff --git a/docs/css/main.css b/docs-legacy/css/main.css similarity index 100% rename from docs/css/main.css rename to docs-legacy/css/main.css diff --git a/docs/css/syntax.css b/docs-legacy/css/syntax.css similarity index 100% rename from docs/css/syntax.css rename to docs-legacy/css/syntax.css diff --git a/docs/home.md b/docs-legacy/home.md similarity index 100% rename from docs/home.md rename to docs-legacy/home.md diff --git a/docs/icons/github.svg b/docs-legacy/icons/github.svg similarity index 100% rename from docs/icons/github.svg rename to docs-legacy/icons/github.svg diff --git a/docs/icons/menu.svg b/docs-legacy/icons/menu.svg similarity index 100% rename from docs/icons/menu.svg rename to docs-legacy/icons/menu.svg diff --git a/docs/index.md b/docs-legacy/index.md similarity index 100% rename from docs/index.md rename to docs-legacy/index.md diff --git a/docs/script/launch b/docs-legacy/script/launch similarity index 100% rename from docs/script/launch rename to docs-legacy/script/launch diff --git a/docs/static/banner.png b/docs-legacy/static/banner.png similarity index 100% rename from docs/static/banner.png rename to docs-legacy/static/banner.png diff --git a/docs/static/icon_foreground.png b/docs-legacy/static/icon_foreground.png similarity index 100% rename from docs/static/icon_foreground.png rename to docs-legacy/static/icon_foreground.png diff --git a/docs/static/screenshot-1.png b/docs-legacy/static/screenshot-1.png similarity index 100% rename from docs/static/screenshot-1.png rename to docs-legacy/static/screenshot-1.png diff --git a/docs/static/screenshot-2.png b/docs-legacy/static/screenshot-2.png similarity index 100% rename from docs/static/screenshot-2.png rename to docs-legacy/static/screenshot-2.png diff --git a/docs/static/sharechat.png b/docs-legacy/static/sharechat.png similarity index 100% rename from docs/static/sharechat.png rename to docs-legacy/static/sharechat.png From 87200f5f3323227417ca5c621bb22172acffcf68 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 3 Aug 2024 15:22:01 +0200 Subject: [PATCH 03/14] Revisit README.md --- .github/FUNDING.yml | 12 ----- README.md | 113 ++++++++++++++++---------------------------- assets/logo-256.png | Bin 0 -> 75276 bytes 3 files changed, 40 insertions(+), 85 deletions(-) delete mode 100644 .github/FUNDING.yml create mode 100644 assets/logo-256.png diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 47471b0d..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,12 +0,0 @@ -# These are supported funding model platforms - -github: [natario1] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/README.md b/README.md index a0351f3e..9bd98b1e 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,60 @@ -[![Build Status](https://github.com/natario1/Transcoder/workflows/Build/badge.svg?event=push)](https://github.com/natario1/Transcoder/actions) -[![Release](https://img.shields.io/github/release/natario1/Transcoder.svg)](https://github.com/natario1/Transcoder/releases) -[![Issues](https://img.shields.io/github/issues-raw/natario1/Transcoder.svg)](https://github.com/natario1/Transcoder/issues) +[![Build Status](https://github.com/deepmedia/Transcoder/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/deepmedia/Transcoder/actions) +[![Release](https://img.shields.io/github/release/deepmedia/Transcoder.svg)](https://github.com/deepmedia/Transcoder/releases) +[![Issues](https://img.shields.io/github/issues-raw/deepmedia/Transcoder.svg)](https://github.com/deepmedia/Transcoder/issues) -⠀ - -

- -

- -*Looking for a powerful camera library to take videos? Take a look at our [CameraView](https://github.com/natario1/CameraView).* - -*Need support, consulting, or have any other business-related question? Feel free to get in touch.* - -*Like the project, make profit from it, or simply want to thank back? Please consider [sponsoring me](https://github.com/sponsors/natario1)!* +![Project logo](assets/logo-256.png) # Transcoder Transcodes and compresses video files into the MP4 format, with audio support, using hardware-accelerated Android codecs available on the device. Works on API 18+. -```groovy -implementation 'com.otaliastudios:transcoder:0.10.5' -``` - - Fast transcoding to AAC/AVC - Hardware accelerated - Convenient, fluent API - Thumbnails support -- Concatenate multiple video and audio tracks [[docs]](https://natario1.github.io/Transcoder/docs/concatenation) -- Clip or trim video segments [[docs]](https://natario1.github.io/Transcoder/docs/clipping) -- Choose output size, with automatic cropping [[docs]](https://natario1.github.io/Transcoder/docs/track-strategies#video-size) -- Choose output rotation [[docs]](https://natario1.github.io/Transcoder/docs/advanced-options#video-rotation) -- Choose output speed [[docs]](https://natario1.github.io/Transcoder/docs/advanced-options#video-speed) -- Choose output frame rate [[docs]](https://natario1.github.io/Transcoder/docs/track-strategies#other-options) -- Choose output audio channels [[docs]](https://natario1.github.io/Transcoder/docs/track-strategies#audio-strategies) -- Choose output audio sample rate [[docs]](https://natario1.github.io/Transcoder/docs/track-strategies#audio-strategies) -- Override frames timestamp, e.g. to slow down the middle part of the video [[docs]](https://natario1.github.io/Transcoder/docs/advanced-options#time-interpolation) -- Error handling [[docs]](https://natario1.github.io/Transcoder/docs/events) -- Configurable validators to e.g. avoid transcoding if the source is already compressed enough [[docs]](https://natario1.github.io/Transcoder/docs/validators) -- Configurable video and audio strategies [[docs]](https://natario1.github.io/Transcoder/docs/track-strategies) - -⠀ - -

- -

- -⠀ +- Concatenate multiple video and audio tracks [[docs]](https://opensource.deepmedia.io/transcoder/concatenation) +- Clip or trim video segments [[docs]](https://opensource.deepmedia.io/transcoder/clipping) +- Choose output size, with automatic cropping [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies#video-size) +- Choose output rotation [[docs]](https://opensource.deepmedia.io/transcoder/advanced-options#video-rotation) +- Choose output speed [[docs]](https://opensource.deepmedia.io/transcoder/advanced-options#video-speed) +- Choose output frame rate [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies#other-options) +- Choose output audio channels [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies#audio-strategies) +- Choose output audio sample rate [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies#audio-strategies) +- Override frames timestamp, e.g. to slow down the middle part of the video [[docs]](https://opensource.deepmedia.io/transcoder/advanced-options#time-interpolation) +- Error handling [[docs]](https://opensource.deepmedia.io/transcoder/events) +- Configurable validators to e.g. avoid transcoding if the source is already compressed enough [[docs]](https://opensource.deepmedia.io/transcoder/validators) +- Configurable video and audio strategies [[docs]](https://opensource.deepmedia.io/transcoder/track-strategies) + +```kotlin +// build.gradle.kts +dependencies { + implementation("com.otaliastudios:transcoder:0.10.5") +} +``` *This project started as a fork of [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder). -With respect to the source project, which misses most of the functionality listed above, -we have also fixed a huge number of bugs and are much less conservative when choosing options +With respect to the source project, which misses most of the functionality listed above, +we have also fixed a huge number of bugs and are much less conservative when choosing options that might not be supported. The source project will always throw - for example, accepting only 16:9, AVC Baseline Profile videos - we prefer to try and let the codec fail if it wants to*. -## Support - -If you like the project, make profit from it, or simply want to thank back, please consider -[sponsoring me](https://github.com/sponsors/natario1) through the GitHub Sponsors program! -You can have your company logo here, get private support hours or simply help me push this forward. - -Transcoder is trusted and supported by [ShareChat](https://sharechat.com/), a social media app with -over 100 million downloads. +*Transcoder is trusted and supported by [ShareChat](https://sharechat.com/), a social media app with +over 100 million downloads.* -

- -

+Please check out [the official website](https://opensource.deepmedia.io/transcoder) for setup instructions and documentation. +You may also check the demo app (under `/demo`) for a complete example. -Feel free to contact me for support, consulting or any -other business-related question. - -## Setup - -Please read the [official website](https://natario1.github.io/Transcoder) for setup instructions and documentation. -You might also be interested in our [changelog](https://natario1.github.io/Transcoder/about/changelog). -Using Transcoder is extremely simple: - -```java +```kotlin Transcoder.into(filePath) - .addDataSource(context, uri) // or... - .addDataSource(filePath) // or... - .addDataSource(fileDescriptor) // or... - .addDataSource(dataSource) - .setListener(new TranscoderListener() { - public void onTranscodeProgress(double progress) {} - public void onTranscodeCompleted(int successCode) {} - public void onTranscodeCanceled() {} - public void onTranscodeFailed(@NonNull Throwable exception) {} - }).transcode() + .addDataSource(context, uri) // or... + .addDataSource(filePath) // or... + .addDataSource(fileDescriptor) // or... + .addDataSource(dataSource) + .setListener(object : TranscoderListener { + override fun onTranscodeProgress(progress: Double) = Unit + override fun onTranscodeCompleted(successCode: Int) = Unit + override fun onTranscodeCanceled() = Unit + override fun onTranscodeFailed(exception: Throwable) = Unit + }).transcode() ``` - -Take a look at the demo app for a complete example. \ No newline at end of file diff --git a/assets/logo-256.png b/assets/logo-256.png new file mode 100644 index 0000000000000000000000000000000000000000..932c421a0e75c2a08282756d09e669f111a8afad GIT binary patch literal 75276 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

Eq!O+V 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 Date: Sat, 3 Aug 2024 15:38:55 +0200 Subject: [PATCH 04/14] Review github workflows and secondary markdown files --- .github/CODE_OF_CONDUCT.md | 2 +- .github/CONTRIBUTING.md | 31 ++++++++++++++++++++++++- .github/workflows/build.yml | 42 ++++++++++++++++++---------------- .github/workflows/deploy.yml | 16 ++++++++----- .github/workflows/snapshot.yml | 12 ++++++---- LICENSE | 2 +- 6 files changed, 71 insertions(+), 34 deletions(-) diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 0df8b1dc..7cfaf5e8 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mat.iavarone@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@deepmedia.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 89da4fab..b9763c60 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1 +1,30 @@ -Contributing guidelines are [hosted here](https://natario1.github.io/Transcoder/extra/contributing). \ No newline at end of file + +Everyone is welcome to contribute with suggestions or pull requests. We are grateful to anyone who will contribute with fixes, features or feature requests. + +### Bug reports + +Please make sure to fill the bug report issue template on GitHub, if applicable. +We highly recommend to try to reproduce the bug in the demo app, as this helps a lot in debugging +and excludes programming errors from your side. + +Make sure to include: + +- A clear and concise description of what the bug is +- Transcoder version, device type, Android API level +- Exact steps to reproduce the issue +- Description of the expected behavior +- The original media file(s) that manifest the problem + +Recommended extras: + +- LogCat logs (use `Logger.setLogLevel(LEVEL_VERBOSE)` to print all) +- Link to a GitHub repo where the bug is reproducible + +### Pull Requests + +Please open an issue first! + +Unless your PR is a simple fix (typos, documentation, bugs with obvious solution), opening an issue +will let us discuss the problem, take design decisions and have a reference to the issue description. + +If you can, please write tests. We are planning to work on improving the library test coverage soon. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7f27098..b1265ceb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,4 @@ # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions -# Renaming ? Change the README badge. name: Build on: push: @@ -10,48 +9,51 @@ jobs: ANDROID_BASE_CHECKS: name: Base Checks runs-on: ubuntu-latest + env: + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: temurin cache: gradle - name: Perform base checks - run: ./gradlew demo:assembleDebug lib:publishToDirectory + run: ./gradlew demo:assembleDebug lib:deployLocal ANDROID_EMULATOR_TESTS: name: Emulator Tests runs-on: macos-latest strategy: fail-fast: false matrix: - EMULATOR_API: [22, 25, 28] - include: - - EMULATOR_API: 28 - EMULATOR_ARCH: x86_64 - - EMULATOR_API: 25 - EMULATOR_ARCH: x86 - - EMULATOR_API: 22 - EMULATOR_ARCH: x86 + EMULATOR_API: [21, 23, 29] steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: temurin cache: gradle + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Execute emulator tests timeout-minutes: 30 uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.EMULATOR_API }} - arch: ${{ matrix.EMULATOR_ARCH }} - disable-animations: true - profile: Nexus 5X + arch: x86_64 + profile: Nexus 6 emulator-options: -no-snapshot -no-window -no-boot-anim -camera-back none -camera-front none -gpu swiftshader_indirect script: ./.github/workflows/emulator_script.sh + - name: Upload emulator tests artifact - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: emulator_tests_${{ matrix.EMULATOR_API }} path: ./lib/build/outputs/code_coverage/debugAndroidTest/connected \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 523e092f..e7fcc894 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,19 +5,23 @@ on: types: [published] jobs: MAVEN_UPLOAD: - name: Maven Upload + name: Maven Central Upload runs-on: ubuntu-latest env: SIGNING_KEY: ${{ secrets.SIGNING_KEY }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} SONATYPE_USER: ${{ secrets.SONATYPE_USER }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + GHUB_USER: ${{ secrets.GHUB_USER }} + GHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GHUB_PERSONAL_ACCESS_TOKEN }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: temurin cache: gradle - - name: Perform maven upload - run: ./gradlew publishToSonatype + - name: Publish to Maven Central + run: ./gradlew deployNexus + - name: Publish to GitHub Packages + run: ./gradlew deployGithub diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index db169d11..370794bf 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -14,12 +14,14 @@ jobs: SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} SONATYPE_USER: ${{ secrets.SONATYPE_USER }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + GHUB_USER: ${{ secrets.GHUB_USER }} + GHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GHUB_PERSONAL_ACCESS_TOKEN }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: temurin cache: gradle - - name: Publish sonatype snapshot - run: ./gradlew publishToSonatypeSnapshot \ No newline at end of file + - name: Publish nexus snapshot + run: ./gradlew deployNexusSnapshot \ No newline at end of file diff --git a/LICENSE b/LICENSE index 8dada3ed..119434ee 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + Copyright 2024 DeepMedia Srl Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 3310df891ccf5787dbbf8ae14596e8addb1ac7a5 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 3 Aug 2024 18:13:32 +0200 Subject: [PATCH 05/14] Review build files --- build.gradle.kts | 18 +--- demo/build.gradle.kts | 18 ++-- demo/src/main/AndroidManifest.xml | 3 +- .../transcoder/demo/ThumbnailerActivity.java | 39 ++++---- .../transcoder/demo/TranscoderActivity.java | 92 +++++++++---------- gradle.properties | 25 +---- gradle/wrapper/gradle-wrapper.properties | 2 +- lib/build.gradle.kts | 33 +++---- lib/src/main/AndroidManifest.xml | 5 +- settings.gradle.kts | 18 +++- 10 files changed, 108 insertions(+), 145 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a697c900..ccbc726c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,9 @@ -buildscript { +/* buildscript { extra["minSdkVersion"] = 18 extra["compileSdkVersion"] = 31 extra["targetSdkVersion"] = 31 - + repositories { google() mavenCentral() @@ -15,16 +15,4 @@ buildscript { classpath("com.android.tools.build:gradle:7.0.2") classpath("io.deepmedia.tools:publisher:0.6.0") } -} - -allprojects { - repositories { - google() - mavenCentral() - jcenter() - } -} - -tasks.register("clean", Delete::class) { - delete(buildDir) -} \ No newline at end of file +} */ diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index be206540..8311203e 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -1,23 +1,17 @@ plugins { - id("com.android.application") - id("kotlin-android") + id("com.android.application") version "8.2.2" + kotlin("android") version "2.0.0" } android { - setCompileSdkVersion(property("compileSdkVersion") as Int) - + namespace = "com.otaliastudios.transcoder.demo" + compileSdk = 34 defaultConfig { - applicationId = "com.otaliastudios.transcoder.demo" - minSdk = property("minSdkVersion") as Int - targetSdk = property("targetSdkVersion") as Int + minSdk = 18 + targetSdk = 34 versionCode = 1 versionName = "1.0" } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } } dependencies { diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 7390e0d5..5da9cd95 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> 0) { builder.addResizer(new AspectRatioResizer(aspectRatio)); } - float fraction; - switch (mVideoResolutionGroup.getCheckedRadioButtonId()) { - case R.id.resolution_half: fraction = 0.5F; break; - case R.id.resolution_third: fraction = 1F / 3F; break; - default: fraction = 1F; - } + + float fraction = 1F; + int fractionId = mVideoResolutionGroup.getCheckedRadioButtonId(); + if (fractionId == R.id.resolution_half) fraction = 0.5F; + else if (fractionId == R.id.resolution_third) fraction = 1F / 3F; builder.addResizer(new FractionResizer(fraction)); - int rotation; - switch (mVideoRotationGroup.getCheckedRadioButtonId()) { - case R.id.rotation_90: rotation = 90; break; - case R.id.rotation_180: rotation = 180; break; - case R.id.rotation_270: rotation = 270; break; - default: rotation = 0; - } + + + int rotation = 0; + int rotationId = mVideoRotationGroup.getCheckedRadioButtonId(); + if (rotationId == R.id.rotation_90) rotation = 90; + else if (rotationId == R.id.rotation_180) rotation = 180; + else if (rotationId == R.id.rotation_270) rotation = 270; builder.setRotation(rotation); // Launch the transcoding operation. diff --git a/demo/src/main/java/com/otaliastudios/transcoder/demo/TranscoderActivity.java b/demo/src/main/java/com/otaliastudios/transcoder/demo/TranscoderActivity.java index ea904de9..c07ef4bb 100644 --- a/demo/src/main/java/com/otaliastudios/transcoder/demo/TranscoderActivity.java +++ b/demo/src/main/java/com/otaliastudios/transcoder/demo/TranscoderActivity.java @@ -159,24 +159,21 @@ protected void onCreate(Bundle savedInstanceState) { } private void syncParameters() { - int channels; - switch (mAudioChannelsGroup.getCheckedRadioButtonId()) { - case R.id.channels_mono: channels = 1; break; - case R.id.channels_stereo: channels = 2; break; - default: channels = DefaultAudioStrategy.CHANNELS_AS_INPUT; - } - int sampleRate; - switch (mAudioSampleRateGroup.getCheckedRadioButtonId()) { - case R.id.sampleRate_32: sampleRate = 32000; break; - case R.id.sampleRate_48: sampleRate = 48000; break; - default: sampleRate = DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT; - } - boolean removeAudio; - switch (mAudioReplaceGroup.getCheckedRadioButtonId()) { - case R.id.replace_remove: removeAudio = true; break; - case R.id.replace_yes: removeAudio = false; break; - default: removeAudio = false; - } + int channels = DefaultAudioStrategy.CHANNELS_AS_INPUT; + int channelsId = mAudioChannelsGroup.getCheckedRadioButtonId(); + if (channelsId == R.id.channels_mono) channels = 1; + else if (channelsId == R.id.channels_stereo) channels = 2; + + int sampleRate = DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT; + int sampleRateId = mAudioSampleRateGroup.getCheckedRadioButtonId(); + if (sampleRateId == R.id.sampleRate_32) sampleRate = 32000; + else if (sampleRateId == R.id.sampleRate_48) sampleRate = 48000; + + boolean removeAudio = false; + int removeAudioId = mAudioReplaceGroup.getCheckedRadioButtonId(); + if (removeAudioId == R.id.replace_remove) removeAudio = true; + else if (removeAudioId == R.id.replace_yes) removeAudio = false; + if (removeAudio) { mTranscodeAudioStrategy = new RemoveTrackStrategy(); } else { @@ -186,26 +183,23 @@ private void syncParameters() { .build(); } - int frames; - switch (mVideoFramesGroup.getCheckedRadioButtonId()) { - case R.id.frames_24: frames = 24; break; - case R.id.frames_30: frames = 30; break; - case R.id.frames_60: frames = 60; break; - default: frames = DefaultVideoStrategy.DEFAULT_FRAME_RATE; - } - float fraction; - switch (mVideoResolutionGroup.getCheckedRadioButtonId()) { - case R.id.resolution_half: fraction = 0.5F; break; - case R.id.resolution_third: fraction = 1F / 3F; break; - default: fraction = 1F; - } - float aspectRatio; - switch (mVideoAspectGroup.getCheckedRadioButtonId()) { - case R.id.aspect_169: aspectRatio = 16F / 9F; break; - case R.id.aspect_43: aspectRatio = 4F / 3F; break; - case R.id.aspect_square: aspectRatio = 1F; break; - default: aspectRatio = 0F; - } + int frames = DefaultVideoStrategy.DEFAULT_FRAME_RATE; + int framesId = mVideoFramesGroup.getCheckedRadioButtonId(); + if (framesId == R.id.frames_24) frames = 24; + else if (framesId == R.id.frames_30) frames = 30; + else if (framesId == R.id.frames_60) frames = 60; + + float fraction = 1F; + int fractionId = mVideoResolutionGroup.getCheckedRadioButtonId(); + if (fractionId == R.id.resolution_half) fraction = 0.5F; + else if (fractionId == R.id.resolution_third) fraction = 1F / 3F; + + float aspectRatio = 0F; + int aspectRatioId = mVideoAspectGroup.getCheckedRadioButtonId(); + if (aspectRatioId == R.id.aspect_169) aspectRatio = 16F / 9F; + else if (aspectRatioId == R.id.aspect_43) aspectRatio = 4F / 3F; + else if (aspectRatioId == R.id.aspect_square) aspectRatio = 1F; + mTranscodeVideoStrategy = new DefaultVideoStrategy.Builder() .addResizer(aspectRatio > 0 ? new AspectRatioResizer(aspectRatio) : new PassThroughResizer()) .addResizer(new FractionResizer(fraction)) @@ -275,20 +269,16 @@ private void transcode(@NonNull Uri... uris) { return; } - int rotation; - switch (mVideoRotationGroup.getCheckedRadioButtonId()) { - case R.id.rotation_90: rotation = 90; break; - case R.id.rotation_180: rotation = 180; break; - case R.id.rotation_270: rotation = 270; break; - default: rotation = 0; - } + int rotation = 0; + int rotationId = mVideoRotationGroup.getCheckedRadioButtonId(); + if (rotationId == R.id.rotation_90) rotation = 90; + else if (rotationId == R.id.rotation_180) rotation = 180; + else if (rotationId == R.id.rotation_270) rotation = 270; - float speed; - switch (mSpeedGroup.getCheckedRadioButtonId()) { - case R.id.speed_05x: speed = 0.5F; break; - case R.id.speed_2x: speed = 2F; break; - default: speed = 1F; - } + float speed = 1F; + int speedId = mSpeedGroup.getCheckedRadioButtonId(); + if (speedId == R.id.speed_05x) speed = 0.5F; + else if (speedId == R.id.speed_2x) speed = 2F; // Launch the transcoding operation. mTranscodeStartTime = SystemClock.uptimeMillis(); diff --git a/gradle.properties b/gradle.properties index d97d827a..4cd7d5bf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,23 +1,6 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Settings specified in this file will override any Gradle settings -# configured through the IDE. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx10248m -XX:MaxPermSize=256m -# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true - -android.enableJetifier=true android.useAndroidX=true -org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m -# ^ https://github.com/Kotlin/dokka/issues/1405 \ No newline at end of file +org.gradle.caching=true +org.gradle.caching.debug=false +org.gradle.parallel=true +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 26adea71..254dc618 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index efe27254..4ae85699 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -1,36 +1,33 @@ -import io.deepmedia.tools.publisher.common.GithubScm -import io.deepmedia.tools.publisher.common.License -import io.deepmedia.tools.publisher.common.Release -import io.deepmedia.tools.publisher.sonatype.Sonatype - plugins { - id("com.android.library") - id("kotlin-android") - id("io.deepmedia.tools.publisher") + id("com.android.library") version "8.2.2" + kotlin("android") version "2.0.0" + // id("io.deepmedia.tools.deployer") version "0.13.0-rc1" } android { - setCompileSdkVersion(property("compileSdkVersion") as Int) + namespace = "com.otaliastudios.transcoder" + compileSdk = 34 defaultConfig { - minSdk = property("minSdkVersion") as Int - targetSdk = property("targetSdkVersion") as Int + minSdk = 18 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - buildTypes["release"].isMinifyEnabled = false } +kotlin { + jvmToolchain(17) +} dependencies { api("com.otaliastudios.opengl:egloo:0.6.1") - api("androidx.annotation:annotation:1.2.0") + api("androidx.annotation:annotation:1.8.1") - androidTestImplementation("androidx.test:runner:1.4.0") - androidTestImplementation("androidx.test:rules:1.4.0") - androidTestImplementation("androidx.test.ext:junit:1.1.3") + androidTestImplementation("androidx.test:runner:1.6.1") + androidTestImplementation("androidx.test:rules:1.6.1") + androidTestImplementation("androidx.test.ext:junit:1.2.1") androidTestImplementation("org.mockito:mockito-android:2.28.2") } -publisher { +/* publisher { project.description = "Accelerated video transcoding using Android MediaCodec API without native code (no LGPL/patent issues)." project.artifact = "transcoder" project.group = "com.otaliastudios" @@ -59,4 +56,4 @@ publisher { signing.key = "SIGNING_KEY" signing.password = "SIGNING_PASSWORD" } -} +} */ diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml index f62d05a4..48652056 100644 --- a/lib/src/main/AndroidManifest.xml +++ b/lib/src/main/AndroidManifest.xml @@ -1,6 +1,3 @@ - - + - diff --git a/settings.gradle.kts b/settings.gradle.kts index ea9f578b..47611a61 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,17 @@ -include(":lib", ":demo") +pluginManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + google() + } +} + +include(":lib") +include(":demo") From 36d8f14fc3f256c42d348dc3b2a8645885ff2454 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 3 Aug 2024 18:35:09 +0200 Subject: [PATCH 06/14] Fix demo app and tests --- .github/workflows/build.yml | 4 ++-- build.gradle.kts | 18 ------------------ lib/build.gradle.kts | 3 +++ 3 files changed, 5 insertions(+), 20 deletions(-) delete mode 100644 build.gradle.kts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1265ceb..bf6c1f60 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: strategy: fail-fast: false matrix: - EMULATOR_API: [21, 23, 29] + EMULATOR_API: [23, 25, 29] steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 @@ -56,4 +56,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: emulator_tests_${{ matrix.EMULATOR_API }} - path: ./lib/build/outputs/code_coverage/debugAndroidTest/connected \ No newline at end of file + path: ./lib/build/reports/androidTests/connected/debug/ \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index ccbc726c..00000000 --- a/build.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -/* buildscript { - - extra["minSdkVersion"] = 18 - extra["compileSdkVersion"] = 31 - extra["targetSdkVersion"] = 31 - - repositories { - google() - mavenCentral() - jcenter() - } - - dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30") - classpath("com.android.tools.build:gradle:7.0.2") - classpath("io.deepmedia.tools:publisher:0.6.0") - } -} */ diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 4ae85699..6eaa457b 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -11,6 +11,9 @@ android { minSdk = 18 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + testOptions { + targetSdk = 23 + } } kotlin { From 0a6e10522f68fd24e2f681157ae334edc80ac032 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sun, 4 Aug 2024 14:50:49 +0200 Subject: [PATCH 07/14] Multiplatform publications --- build.gradle.kts | 5 +++ demo/build.gradle.kts | 8 ++-- lib/build.gradle.kts | 102 +++++++++++++++++++++++++++++------------- settings.gradle.kts | 1 + 4 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 build.gradle.kts diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..1e81addf --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + kotlin("android") version "2.0.0" apply false + id("com.android.library") version "8.2.2" apply false + id("com.android.application") version "8.2.2" apply false +} \ No newline at end of file diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 8311203e..75602280 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("com.android.application") version "8.2.2" - kotlin("android") version "2.0.0" + id("com.android.application") + kotlin("android") } android { @@ -16,6 +16,6 @@ android { dependencies { implementation(project(":lib")) - implementation("com.google.android.material:material:1.4.0") - implementation("androidx.appcompat:appcompat:1.3.1") + implementation("com.google.android.material:material:1.12.0") + implementation("androidx.appcompat:appcompat:1.7.0") } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 6eaa457b..a7de0e56 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -1,7 +1,8 @@ plugins { - id("com.android.library") version "8.2.2" - kotlin("android") version "2.0.0" - // id("io.deepmedia.tools.deployer") version "0.13.0-rc1" + id("com.android.library") + kotlin("android") + id("io.deepmedia.tools.deployer") version "0.14.0-local-alpha1" + id("org.jetbrains.dokka") version "1.9.20" } android { @@ -14,6 +15,9 @@ android { testOptions { targetSdk = 23 } + publishing { + singleVariant("release") + } } kotlin { @@ -28,35 +32,71 @@ dependencies { androidTestImplementation("androidx.test:rules:1.6.1") androidTestImplementation("androidx.test.ext:junit:1.2.1") androidTestImplementation("org.mockito:mockito-android:2.28.2") + + dokkaPlugin("org.jetbrains.dokka:android-documentation-plugin:1.9.20") } -/* publisher { - project.description = "Accelerated video transcoding using Android MediaCodec API without native code (no LGPL/patent issues)." - project.artifact = "transcoder" - project.group = "com.otaliastudios" - project.url = "https://github.com/natario1/Transcoder" - project.scm = GithubScm("natario1", "Transcoder") - project.addLicense(License.APACHE_2_0) - project.addDeveloper("natario1", "mat.iavarone@gmail.com") - release.sources = Release.SOURCES_AUTO - release.docs = Release.DOCS_AUTO - release.version = "0.10.5" - - directory() - - sonatype { - auth.user = "SONATYPE_USER" - auth.password = "SONATYPE_PASSWORD" - signing.key = "SIGNING_KEY" - signing.password = "SIGNING_PASSWORD" - } - - sonatype("snapshot") { - repository = Sonatype.OSSRH_SNAPSHOT_1 +val javadocs = tasks.register("dokkaJavadocJar") { + dependsOn(tasks.dokkaJavadoc) + from(tasks.dokkaJavadoc.flatMap { it.outputDirectory }) + archiveClassifier.set("javadoc") +} + +deployer { + verbose = true + + content { + component { + fromSoftwareComponent("release") + kotlinSources() + docs(javadocs) + } + } + + projectInfo { + groupId = "com.otaliastudios" + artifactId = "transcoder" + release.version = "0.10.5" + release.tag = "v0.10.5" + description = "Accelerated video transcoding using Android MediaCodec API without native code (no LGPL/patent issues)." + url = "https://github.com/deepmedia/Transcoder" + scm.fromGithub("deepmedia", "Transcoder") + license(apache2) + developer("natario1", "mattia@deepmedia.io", "DeepMedia", "https://deepmedia.io") + } + + signing { + key = secret("SIGNING_KEY") + password = secret("SIGNING_PASSWORD") + } + + // use "deployLocal" to deploy to local maven repository + localSpec { + directory.set(rootProject.layout.buildDirectory.get().dir("inspect")) + } + + // use "deployNexus" to deploy to OSSRH / maven central + nexusSpec { + auth.user = secret("SONATYPE_USER") + auth.password = secret("SONATYPE_PASSWORD") + syncToMavenCentral = true + } + + // use "deployNexusSnapshot" to deploy to sonatype snapshots repo + nexusSpec("snapshot") { + auth.user = secret("SONATYPE_USER") + auth.password = secret("SONATYPE_PASSWORD") + repositoryUrl = ossrhSnapshots1 release.version = "latest-SNAPSHOT" - auth.user = "SONATYPE_USER" - auth.password = "SONATYPE_PASSWORD" - signing.key = "SIGNING_KEY" - signing.password = "SIGNING_PASSWORD" } -} */ + + // use "deployGithub" to deploy to github packages + githubSpec { + repository = "Transcoder" + owner = "deepmedia" + auth { + user = secret("GHUB_USER") + token = secret("GHUB_PERSONAL_ACCESS_TOKEN") + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 47611a61..b3b62bfb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,6 @@ pluginManagement { repositories { + mavenLocal() google() gradlePluginPortal() mavenCentral() From aaec31c959485fc3ad96bb385359c1da16ce7469 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sun, 4 Aug 2024 14:53:00 +0200 Subject: [PATCH 08/14] Idea files --- .gitignore | 1 - .idea/.gitignore | 3 +++ .idea/compiler.xml | 6 ++++++ .idea/deploymentTargetDropDown.xml | 10 ++++++++++ .idea/gradle.xml | 19 +++++++++++++++++++ .idea/kotlinc.xml | 6 ++++++ .idea/misc.xml | 5 +++++ .idea/runConfigurations/deployLocal.xml | 24 ++++++++++++++++++++++++ .idea/vcs.xml | 6 ++++++ 9 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/kotlinc.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations/deployLocal.xml create mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index 79859d12..d680f1ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .gradle /local.properties -/.idea .DS_Store /build *.iml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..b589d56e --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 00000000..81d2a465 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..96913344 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 00000000..6d0ee1c2 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..53b7407a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/deployLocal.xml b/.idea/runConfigurations/deployLocal.xml new file mode 100644 index 00000000..f7ebf23b --- /dev/null +++ b/.idea/runConfigurations/deployLocal.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From a7ebd9c1feb13ae2f0d3350ac78f586fc7a0ed6a Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Mon, 5 Aug 2024 09:17:00 +0200 Subject: [PATCH 09/14] Docs migration --- docs-legacy/README.md | 2 +- docs-legacy/_about/changelog.md | 182 +----------------------- docs-legacy/_about/getting-started.md | 38 +---- docs-legacy/_about/install.md | 37 +---- docs-legacy/_config.yml | 1 - docs-legacy/_docs/advanced-options.md | 100 +------------ docs-legacy/_docs/clipping.md | 53 +------ docs-legacy/_docs/concatenation.md | 56 +------- docs-legacy/_docs/data-sources.md | 55 +------- docs-legacy/_docs/events.md | 55 +------- docs-legacy/_docs/track-strategies.md | 151 +------------------- docs-legacy/_docs/validators.md | 47 +------ docs-legacy/_extra/contact.md | 8 +- docs-legacy/_extra/contributing.md | 56 +------- docs-legacy/_extra/donate.md | 12 +- docs-legacy/home.md | 45 +----- docs-legacy/index.md | 2 +- docs/README.md | 1 + docs/advanced-options.mdx | 101 ++++++++++++++ docs/changelog.mdx | 193 ++++++++++++++++++++++++++ docs/clipping.mdx | 56 ++++++++ docs/concatenation.mdx | 60 ++++++++ docs/data-sources.mdx | 60 ++++++++ docs/events.mdx | 55 ++++++++ docs/index.mdx | 81 +++++++++++ docs/install.mdx | 37 +++++ docs/track-strategies.mdx | 153 ++++++++++++++++++++ docs/validators.mdx | 50 +++++++ lib/build.gradle.kts | 2 +- 29 files changed, 864 insertions(+), 885 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/advanced-options.mdx create mode 100644 docs/changelog.mdx create mode 100644 docs/clipping.mdx create mode 100644 docs/concatenation.mdx create mode 100644 docs/data-sources.mdx create mode 100644 docs/events.mdx create mode 100644 docs/index.mdx create mode 100644 docs/install.mdx create mode 100644 docs/track-strategies.mdx create mode 100644 docs/validators.mdx diff --git a/docs-legacy/README.md b/docs-legacy/README.md index 9312dc35..995fe576 100644 --- a/docs-legacy/README.md +++ b/docs-legacy/README.md @@ -1 +1 @@ -Read the docs at https://natario1.github.io/Transcoder . +Read the docs at https://opensource.deepmedia.io/transcoder. diff --git a/docs-legacy/_about/changelog.md b/docs-legacy/_about/changelog.md index 283772c7..575f5a04 100644 --- a/docs-legacy/_about/changelog.md +++ b/docs-legacy/_about/changelog.md @@ -2,186 +2,6 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder/changelog title: "Changelog" -order: 3 --- -New versions are released through GitHub, so the reference page is the [GitHub Releases](https://github.com/natario1/Transcoder/releases) page. - -> Starting from 0.7.0, you can now [support development](https://github.com/sponsors/natario1) through the GitHub Sponsors program. -Companies can share a tiny part of their revenue and get private support hours in return. Thanks! - -### v0.10.5 - -- Fix: Honor buffer.hasRemaining() and otherwise release the buffer. ([#182][182]) -- Fix: Call progress even when not advenced. ([#170][170]) -- Fix: FilePathDataSource crash before initialize. ([#160][160]) - - - -### v0.10.4 - -- Fix: fixed crash in specific conditions ([#140][140]) -- New: AssetFileDescriptorDataSource, can be used to transcode from AssetFileDescriptors ([#140][140]) - - - -### v0.10.3 - -- Fix: error when merging many files, thanks to [@DamonChen117][DamonChen117] ([#134][134]) -- Fix: more concatenation issues - - - -### v0.10.2 - -- Fix: error when merging many files ([#132][132]) - - - -### v0.10.1 - -- Fix: thumbnails upside down ([#125][125]) -- Fix: clip/trim issues ([#127][127]) -- Fix: seeking issues ([#128][128]) -- Fix: concatenation failure ([#130][130]) - - - -## v0.10.0 - -> Transcoder is now distributed through Maven Central. Snapshot releases are available as well. - -- New: thumbnails support ([#119][119]) -- Improvement: rewritten transcoding pipeline ([#118][118]) -- Fix: many bugs fixed while rewriting the pipeline ([#118][118]) - - - -### v0.9.1 - -- Improvement: `DefaultDataSink` new constructor with support for FileDescriptor. ([#87][87]) - - - -### v0.9.0 - -- New: `BlankAudioDataSource` can be used to add muted audio to a video-only track, thanks to [@mudar][mudar] ([#64][64]) -- Enhancement: you can now concatenate multiple files even if some of them have no audio, thanks to [@mudar][mudar] ([#64][64]) -- Enhancement: you can now concatenate multiple files without audio track, thanks to [@cbernier2][cbernier2] ([#61][61]) - - - -### v0.8.0 - -- New: `TrimDataSource` to trim segments. Use it to wrap your original source. Thanks to [@mudar][mudar] ([#50][50]) -- New: `ClipDataSource`, just likes `TrimDataSource` but selects trim values with respect to video start ([#54][54]) - -> Transcoder will trim video segments only at the closest video sync frame. If your video has few sync -frames, the trim timestamp might be different than what was selected. - - - -##### v0.7.4 - -- Fix: fixed Xamarin incompatibility, thanks to [@aweck][aweck] ([#41][41]) -- Fix: fixed small bugs with specific API versions / media files ([#47][47]) -- Fix: fixed issues with specific media files, ensure consistent onProgress callback ([#48][48]) - - - -##### v0.7.3 - -- Fix: fixed bug with files that do not have an audio track, thanks to [@pawegio][pawegio] ([#31][31]) -- Fix: fixed possible issues with FilePathDataSource ([#32][32]) - - - -##### v0.7.2 - -- Improvement: better input format detection. Fixes bugs with certain files ([#29][29]) -- Improvement: added `DefaultAudioStrategy.Builder.bitRate()` option ([#29][29]) - - - -##### v0.7.1 - -- Improvement: update the underlying OpenGL library ([#20][20]) - - - -### v0.7.0 - -- New: video concatenation to stitch together multiple media ([#14][14]) -- New: select a specific track type (`VIDEO` or `AUDIO`) for sources ([#14][14]) -- New: audio resampling through `DefaultAudioStrategy` ([#16][16]) -- New: custom resampling through `TranscoderOptions.setAudioResampler()` ([#16][16]) -- Breaking change: `TranscoderOptions.setDataSource()` renamed to `addDataSource()` ([#14][14]) -- Breaking change: `TranscoderOptions.setRotation()` renamed to `setVideoRotation()` ([#14][14]) -- Breaking change: `DefaultVideoStrategy.iFrameInterval()` renamed to `keyFrameInterval()` ([#14][14]) -- Breaking change: `DefaultAudioStrategy` now uses a builder - removed old constructor ([#16][16]) -- Improvement: rotate videos through OpenGL instead of using metadata ([#14][14]) -- Improvement: when concatenating multiple sources, automatically clip the longer track (audio or video) ([#17][17]) -- Improvement: various bug fixed ([#18][18]) - - - -### v0.6.0 - -- New: ability to change video/audio speed and change each frame timestamp ([#10][10]) -- New: ability to set the video output rotation ([#8][8]) -- Improvement: new frame dropping algorithm, thanks to [@Saqrag][Saqrag] ([#9][9]) -- Improvement: avoid format validation on tracks coming from PassThroughTrackTranscoder, thanks to [@Saqrag][Saqrag] ([#11][11]) - - - -### v0.5.0 - -- New: video cropping to any dimension. Encoder will crop the exceeding size. ([#6][6]) -- New: `AspectRatioResizer` to crop to a given aspect ratio. ([#6][6]) -- Breaking change: `MediaTranscoder` renamed to `Transcoder`. ([#6][6]) -- Breaking change: `MediaTranscoderOptions` renamed to `TranscoderOptions`. ([#6][6]) -- Breaking change: `MediaTranscoder.Listener` renamed to `TranscoderListener`. ([#6][6]) -- Improvement: use [EglCore](https://github.com/natario1/EglCore) to replace GL logic. ([#5][5]) -- Improvement: bug fixes and a new demo app to test transcoding options easily ([#4][4]) - -[Saqrag]: https://github.com/Saqrag -[pawegio]: https://github.com/pawegio -[aweck]: https://github.com/aweck -[mudar]: https://github.com/mudar -[cbernier2]: https://github.com/cbernier2 -[DamonChen117]: https://github.com/DamonChen117 - -[4]: https://github.com/natario1/Transcoder/pull/4 -[5]: https://github.com/natario1/Transcoder/pull/5 -[6]: https://github.com/natario1/Transcoder/pull/6 -[8]: https://github.com/natario1/Transcoder/pull/8 -[9]: https://github.com/natario1/Transcoder/pull/9 -[10]: https://github.com/natario1/Transcoder/pull/10 -[14]: https://github.com/natario1/Transcoder/pull/14 -[16]: https://github.com/natario1/Transcoder/pull/16 -[17]: https://github.com/natario1/Transcoder/pull/17 -[18]: https://github.com/natario1/Transcoder/pull/18 -[20]: https://github.com/natario1/Transcoder/pull/20 -[29]: https://github.com/natario1/Transcoder/pull/29 -[31]: https://github.com/natario1/Transcoder/pull/31 -[32]: https://github.com/natario1/Transcoder/pull/32 -[41]: https://github.com/natario1/Transcoder/pull/41 -[47]: https://github.com/natario1/Transcoder/pull/47 -[48]: https://github.com/natario1/Transcoder/pull/48 -[50]: https://github.com/natario1/Transcoder/pull/50 -[54]: https://github.com/natario1/Transcoder/pull/54 -[61]: https://github.com/natario1/Transcoder/pull/61 -[64]: https://github.com/natario1/Transcoder/pull/64 -[87]: https://github.com/natario1/Transcoder/pull/87 -[118]: https://github.com/natario1/Transcoder/pull/118 -[119]: https://github.com/natario1/Transcoder/pull/119 -[125]: https://github.com/natario1/Transcoder/pull/125 -[127]: https://github.com/natario1/Transcoder/pull/127 -[128]: https://github.com/natario1/Transcoder/pull/128 -[130]: https://github.com/natario1/Transcoder/pull/130 -[132]: https://github.com/natario1/Transcoder/pull/132 -[134]: https://github.com/natario1/Transcoder/pull/134 -[140]: https://github.com/natario1/Transcoder/pull/140 -[160]: https://github.com/natario1/Transcoder/pull/160 -[170]: https://github.com/natario1/Transcoder/pull/170 -[182]: https://github.com/natario1/Transcoder/pull/182 +Migrated to https://opensource.deepmedia.io/transcoder/changelog \ No newline at end of file diff --git a/docs-legacy/_about/getting-started.md b/docs-legacy/_about/getting-started.md index 11b8f971..35eda638 100644 --- a/docs-legacy/_about/getting-started.md +++ b/docs-legacy/_about/getting-started.md @@ -2,42 +2,6 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder/install title: "Getting Started" -description: "Simple guide to transcode your first video" -order: 2 -disqus: 1 --- -### Before you start - -If your app targets versions older than API 18, you can override the minSdkVersion by -adding this line to your manifest file: - -```xml - -``` - -In this case you should check at runtime that API level is at least 18, before -calling any method here. - -### Transcoding your first video - -Transcoding happens through the `Transcoder` class by passing it an output file path, -and one of more input data sources. It's pretty simple: - -```java -Transcoder.into(filePath) - .addDataSource(context, uri) // or... - .addDataSource(filePath) // or... - .addDataSource(fileDescriptor) // or... - .addDataSource(dataSource) - .setListener(new TranscoderListener() { - public void onTranscodeProgress(double progress) {} - public void onTranscodeCompleted(int successCode) {} - public void onTranscodeCanceled() {} - public void onTranscodeFailed(@NonNull Throwable exception) {} - }).transcode() -``` - -However, we offer many APIs and additional features on top that you can read about in the -in-depth documentation. - +Migrated to https://opensource.deepmedia.io/transcoder/install diff --git a/docs-legacy/_about/install.md b/docs-legacy/_about/install.md index 87fc459b..7f4cdead 100644 --- a/docs-legacy/_about/install.md +++ b/docs-legacy/_about/install.md @@ -2,41 +2,6 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder/install title: "Install" -description: "Integrate in your project" -order: 1 --- -Transcoder is publicly hosted on the [Maven Central](https://repo1.maven.org/maven2/com/otaliastudios/) -repository, where you can download the AAR package. To fetch with Gradle, make sure you add the -Maven Central repository in your root projects `build.gradle` file: - -```kotlin -allprojects { - repositories { - mavenCentral() - } -} -``` - -Then simply download the latest version: - -```kotlin -api("com.otaliastudios:transcoder:{{ site.github_version }}") -``` - -> The library works on API 18+, which is the only requirement and should be met by many projects nowadays. - -### Snapshots - -We deploy snapshots on each push to the main branch. If you want to use the latest, unreleased features, -you can do so (at your own risk) by adding the snapshot repository: - -```kotlin -allprojects { - repositories { - maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") - } -} -``` - -and changing the library version from `{{ site.github_version }}` to `latest-SNAPSHOT`. \ No newline at end of file +Migrated to https://opensource.deepmedia.io/transcoder/install diff --git a/docs-legacy/_config.yml b/docs-legacy/_config.yml index ae6e178d..b5950cf3 100644 --- a/docs-legacy/_config.yml +++ b/docs-legacy/_config.yml @@ -8,7 +8,6 @@ title: Transcoder color: '#f8f8f8' description: A well documented Android library providing hardware-accelerated video transcoding, using MediaCodec APIs instead of native code (no FFMPEG patent issues). Supports cropping to any dimension, concatenation, clipping, audio processing, video speed and much more. # used by ourselves and by seo tag. disqus_shortname: 'natario1-transcoder' -google_analytics_id: 'UA-155077779-2' google_site_verification: '4x49i17ABIrSvUl52SeL0-t0341aTnWWaC62-FYCRT4' github: [metadata] # TODO What's this? github_repo: Transcoder diff --git a/docs-legacy/_docs/advanced-options.md b/docs-legacy/_docs/advanced-options.md index 2a117862..8cb956b5 100644 --- a/docs-legacy/_docs/advanced-options.md +++ b/docs-legacy/_docs/advanced-options.md @@ -2,105 +2,7 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder/advanced-options title: "Advanced Options" -description: "Advanced transcoding options" order: 7 -disqus: 1 --- -### Video rotation - -You can set the output video rotation with the `setRotation(int)` method. This will apply a clockwise -rotation to the input video frames. Accepted values are `0`, `90`, `180`, `270`: - -```java -Transcoder.into(filePath) - .setVideoRotation(rotation) // 0, 90, 180, 270 - // ... -``` - -### Time interpolation - -We offer APIs to change the timestamp of each video and audio frame. You can pass a `TimeInterpolator` -to the transcoder builder to be able to receive the frame timestamp as input, and return a new one -as output. - -```java -Transcoder.into(filePath) - .setTimeInterpolator(timeInterpolator) - // ... -``` - -As an example, this is the implementation of the default interpolator, called `DefaultTimeInterpolator`, -that will just return the input time unchanged: - -```java -@Override -public long interpolate(@NonNull TrackType type, long time) { - // Receive input time in microseconds and return a possibly different one. - return time; -} -``` - -It should be obvious that returning invalid times can make the process crash at any point, or at least -the transcoding operation fail. - -### Video speed - -We also offer a special time interpolator called `SpeedTimeInterpolator` that accepts a `float` parameter -and will modify the video speed. - -- A speed factor equal to 1 will leave speed unchanged -- A speed factor < 1 will slow the video down -- A speed factor > 1 will accelerate the video - -This interpolator can be set using `setTimeInterpolator(TimeInterpolator)`, or, as a shorthand, -using `setSpeed(float)`: - -```java -Transcoder.into(filePath) - .setSpeed(0.5F) // 0.5x - .setSpeed(1F) // Unchanged - .setSpeed(2F) // Twice as fast - // ... -``` - -### Audio stretching - -When a time interpolator alters the frames and samples timestamps, you can either remove audio or -stretch the audio samples to the new length. This is done through the `AudioStretcher` interface: - -```java -Transcoder.into(filePath) - .setAudioStretcher(audioStretcher) - // ... -``` - -The default audio stretcher, `DefaultAudioStretcher`, will: - -- When we need to shrink a group of samples, cut the last ones -- When we need to stretch a group of samples, insert noise samples in between - -Please take a look at the implementation and read class documentation. - -### Audio resampling - -When a sample rate different than the input is specified (by the `TrackStrategy`, or, when using the -default audio strategy, by `DefaultAudioStategy.Builder.sampleRate()`), this library will automatically -perform sample rate conversion for you. - -This operation is performed by a class called `AudioResampler`. We offer the option to pass your -own resamplers through the transcoder builder: - -```java -Transcoder.into(filePath) - .setAudioResampler(audioResampler) - // ... -``` - -The default audio resampler, `DefaultAudioResampler`, will perform both upsampling and downsampling -with very basic algorithms (drop samples when downsampling, repeat samples when upsampling). -Upsampling is generally discouraged - implementing a real upsampling algorithm is probably out of -the scope of this library. - -Please take a look at the implementation and read class documentation. - +Migrated to https://opensource.deepmedia.io/transcoder/advanced-options diff --git a/docs-legacy/_docs/clipping.md b/docs-legacy/_docs/clipping.md index f7d13752..5eb543ba 100644 --- a/docs-legacy/_docs/clipping.md +++ b/docs-legacy/_docs/clipping.md @@ -2,59 +2,8 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder/clipping title: "Clipping and trimming" -description: "How to clip each segment individually on both ends" order: 2 -disqus: 1 --- -Starting from `v0.8.0`, Transcoder offers the option to clip or trim video segments, on one or -both ends. This is done by using special `DataSource` objects that wrap you original source, -so that, in case of [concatenation](concatenation) of multiple media files, the trimming values -can be set individually for each segment. - -> If Transcoder determines that the video should be decoded and re-encoded (status is `TrackStatus.COMPRESSING`) -the clipping position is respected precisely. However, if your [strategy](track-strategies) does not -include video decoding / re-encoding, the clipping position will be moved to the closest video sync frame. -This means that the clipped output duration might be different than expected, -depending on the frequency of sync frames in your original file. - -### TrimDataSource - -The `TrimDataSource` class lets you trim segments by specifying the amount of time to be trimmed -at both ends. For example, the code below will trim the file by 1 second at the beginning, and -2 seconds at the end: - -```java -DataSource source = new UriDataSource(context, uri); -DataSource trim = new TrimDataSource(source, 1000 * 1000, 2 * 1000 * 1000); -Transcoder.into(filePath) - .addDataSource(trim) - .transcode() -``` - -It is recommended to always check `source.getDurationUs()` to compute the correct values. - -### ClipDataSource - -The `ClipDataSource` class lets you clip segments by specifying a time window. For example, -the code below clip the file from second 1 until second 5: - -```java -DataSource source = new UriDataSource(context, uri); -DataSource clip = new ClipDataSource(source, 1000 * 1000, 5 * 1000 * 1000); -Transcoder.into(filePath) - .addDataSource(clip) - .transcode() -``` - -It is recommended to always check `source.getDurationUs()` to compute the correct values. - -### Related APIs - -|Method|Description| -|------|-----------| -|`new TrimDataSource(source, long)`|Creates a new data source trimmed on start.| -|`new TrimDataSource(source, long, long)`|Creates a new data source trimmed on both ends.| -|`new ClipDataSource(source, long)`|Creates a new data source clipped on start.| -|`new ClipDataSource(source, long, long)`|Creates a new data source clipped on both ends.| +Migrated to https://opensource.deepmedia.io/transcoder/clipping diff --git a/docs-legacy/_docs/concatenation.md b/docs-legacy/_docs/concatenation.md index 421e0d59..f09a789c 100644 --- a/docs-legacy/_docs/concatenation.md +++ b/docs-legacy/_docs/concatenation.md @@ -2,61 +2,7 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder/concatenation title: "Concatenation" -description: "How to concatenate video segments" order: 3 -disqus: 1 --- -As you might have guessed from the previous section, you can use `addDataSource(source)` multiple times. -All the source files will be stitched together: - -```java -Transcoder.into(filePath) - .addDataSource(source1) - .addDataSource(source2) - .addDataSource(source3) - // ... -``` - -In the above example, the three videos will be stitched together in the order they are added -to the builder. Once `source1` ends, we'll append `source2` and so on. The library will take care -of applying consistent parameters (frame rate, bit rate, sample rate) during the conversion. - -This is a powerful tool since it can be used per-track: - -```java -Transcoder.into(filePath) - .addDataSource(source1) // Audio & Video, 20 seconds - .addDataSource(TrackType.VIDEO, source2) // Video, 5 seconds - .addDataSource(TrackType.VIDEO, source3) // Video, 5 seconds - .addDataSource(TrackType.AUDIO, source4) // Audio, 10 sceonds - // ... -``` - -In the above example, the output file will be 30 seconds long: - -``` -Video: | •••••••••••••••••• source1 •••••••••••••••••• | •••• source2 •••• | •••• source3 •••• | -Audio: | •••••••••••••••••• source1 •••••••••••••••••• | •••••••••••••• source4 •••••••••••••• | -``` - -And that's all you need to do. - -### Automatic clipping - -When concatenating data from multiple sources and on different tracks, it's common to have -a total audio length that is different than the total video length. - -In this case, `Transcoder` will automatically clip the longest track to match the shorter. -For example: - -```java -Transcoder.into(filePath) - .addDataSource(TrackType.VIDEO, video1) // Video, 30 seconds - .addDataSource(TrackType.VIDEO, video2) // Video, 30 seconds - .addDataSource(TrackType.AUDIO, music) // Audio, 3 minutes - // ... -``` - -In the situation above, we won't use the full music track, but only the first minute of it. - +Migrated to https://opensource.deepmedia.io/transcoder/concatenation diff --git a/docs-legacy/_docs/data-sources.md b/docs-legacy/_docs/data-sources.md index 6b5d935c..274eb541 100644 --- a/docs-legacy/_docs/data-sources.md +++ b/docs-legacy/_docs/data-sources.md @@ -2,61 +2,8 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder/data-sources title: "Data Sources" -description: "Sources of media data" order: 1 -disqus: 1 --- -Starting a transcoding operation will require a source for our data, which is not necessarily -a `File`. The `DataSource` objects will automatically take care about releasing streams / resources, -which is convenient but it means that they can not be used twice. - -```java -Transcoder.into(filePath) - .addDataSource(source1) - .transcode() -``` - -##### UriDataSource - -The Android friendly source can be created with `new UriDataSource(context, uri)` or simply -using `addDataSource(context, uri)` in the transcoding builder. - -##### FileDescriptorDataSource - -A data source backed by a file descriptor. Use `new FileDescriptorDataSource(descriptor)` or -simply `addDataSource(descriptor)` in the transcoding builder. Note that it is the caller -responsibility to close the file descriptor. - -##### FilePathDataSource - -A data source backed by a file absolute path. Use `new FilePathDataSource(path)` or -simply `addDataSource(path)` in the transcoding builder. - -##### AssetFileDescriptorDataSource - -A data source backed by Android's AssetFileDescriptor. Use `new AssetFileDescriptorDataSource(descriptor)` -or simply `addDataSource(descriptor)` in the transcoding builder. Note that it is the caller -responsibility to close the file descriptor. - -### Track specific sources - -Although a media source can have both audio and video, you can select a specific track -for transcoding and exclude the other(s). For example, to select the video track only: - -```java -Transcoder.into(filePath) - .addDataSource(TrackType.VIDEO, source) - .transcode() -``` - -### Related APIs - -|Method|Description| -|------|-----------| -|`addDataSource(Context, Uri)`|Adds a new source for the given Uri.| -|`addDataSource(FileDescriptor)`|Adds a new source for the given FileDescriptor.| -|`addDataSource(String)`|Adds a new source for the given file path.| -|`addDataSource(DataSource)`|Adds a new source.| -|`addDataSource(TrackType, DataSource)`|Adds a new source restricted to the given TrackType.| +Migrated to https://opensource.deepmedia.io/transcoder/data-sources diff --git a/docs-legacy/_docs/events.md b/docs-legacy/_docs/events.md index e66974b2..80e0c86f 100644 --- a/docs-legacy/_docs/events.md +++ b/docs-legacy/_docs/events.md @@ -2,61 +2,8 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder/events title: "Transcoding Events" -description: "Listening to transcoding events" order: 4 -disqus: 1 --- - -Transcoding will happen on a background thread, but we will send updates through the `TranscoderListener` -interface, which can be applied when building the request: - -```java -Transcoder.into(filePath) - .setListenerHandler(handler) - .setListener(new TranscoderListener() { - public void onTranscodeProgress(double progress) {} - public void onTranscodeCompleted(int successCode) {} - public void onTranscodeCanceled() {} - public void onTranscodeFailed(@NonNull Throwable exception) {} - }) - // ... -``` - -All of the listener callbacks are called: - -- If present, on the handler specified by `setListenerHandler()` -- If it has a handler, on the thread that started the `transcode()` call -- As a last resort, on the UI thread - -##### onTranscodeProgress - -This simply sends a double indicating the current progress. The value is typically between 0 and 1, -but can be a negative value to indicate that we are not able to compute progress (yet?). - -This is the right place to update a ProgressBar, for example. - -##### onTranscodeCanceled - -The transcoding operation was canceled. This can happen when the `Future` returned by `transcode()` -is cancelled by the user. - -##### onTranscodeFailed - -This can happen in a number of cases and is typically out of our control. Input options might be -wrong, write permissions might be missing, codec might be absent, input file might be not supported -or simply corrupted. - -You can take a look at the `Throwable` being passed to know more about the exception. - -##### onTranscodeCompleted - -Transcoding operation did succeed. The success code can be: - -|Code|Meaning| -|----|-------| -|`Transcoder.SUCCESS_TRANSCODED`|Transcoding was executed successfully. Transcoded file was written to the output path.| -|`Transcoder.SUCCESS_NOT_NEEDED`|Transcoding was not executed because it was considered **not needed** by the `Validator`.| - -[Keep reading](validators) to know about `Validator`s. +Migrated to https://opensource.deepmedia.io/transcoder/events diff --git a/docs-legacy/_docs/track-strategies.md b/docs-legacy/_docs/track-strategies.md index 00b9ab9b..385de184 100644 --- a/docs-legacy/_docs/track-strategies.md +++ b/docs-legacy/_docs/track-strategies.md @@ -2,156 +2,7 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder/track-strategies title: "Track Strategies" -description: "Per-track transcoding options" order: 6 -disqus: 1 --- - -Track strategies return options for each track (audio or video) for the engine to understand **how** -and **if** this track should be transcoded, and whether the whole process should be aborted. - -```java -Transcoder.into(filePath) - .setVideoTrackStrategy(videoStrategy) - .setAudioTrackStrategy(audioStrategy) - // ... -``` - -The point of `TrackStrategy` is to inspect the input `android.media.MediaFormat` and return -the output `android.media.MediaFormat`, filled with required options. - -This library offers track specific strategies that help with audio and video options (see -[Audio Strategies](#audio-strategies) and [Video Strategies](#video-strategies)). -In addition, we have a few built-in strategies that can work for both audio and video: - -##### PassThroughTrackStrategy - -A TrackStrategy that asks the encoder to keep this track as is, by returning the same input -format. Note that this is risky, as the input track format might not be supported my the MP4 container. - -This will set the `TrackStatus` to `TrackStatus.PASS_THROUGH`. - -##### RemoveTrackStrategy - -A TrackStrategy that asks the encoder to remove this track from the output container, by returning null. -For instance, this can be used as an audio strategy to remove audio from video/audio streams. - -This will set the `TrackStatus` to `TrackStatus.REMOVING`. - -### Audio Strategies - -The default internal strategy for audio is a `DefaultAudioStrategy`, which converts the -audio stream to AAC format with the specified number of channels and [sample rate](advanced-options). - -```java -DefaultAudioStrategy strategy = DefaultAudioStrategy.builder() - .channels(DefaultAudioStrategy.CHANNELS_AS_INPUT) - .channels(1) - .channels(2) - .sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT) - .sampleRate(44100) - .sampleRate(30000) - .bitRate(DefaultAudioStrategy.BITRATE_UNKNOWN) - .bitRate(bitRate) - .build(); - -Transcoder.into(filePath) - .setAudioTrackStrategy(strategy) - // ... -``` - -Take a look at the source code to understand how to manage the `android.media.MediaFormat` object. - -### Video Strategies - -The default internal strategy for video is a `DefaultVideoStrategy`, which converts the -video stream to AVC format and is very configurable. The class helps in defining an output size. -If the output size does not match the aspect ratio of the input stream size, `Transcoder` will -crop part of the input so it matches the final ratio. - -##### Video Size - -We provide helpers for common tasks: - -```java -DefaultVideoStrategy strategy; - -// Sets an exact size. If aspect ratio does not match, cropping will take place. -strategy = DefaultVideoStrategy.exact(1080, 720).build(); - -// Keeps the aspect ratio, but scales down the input size with the given fraction. -strategy = DefaultVideoStrategy.fraction(0.5F).build(); - -// Ensures that each video size is at most the given value - scales down otherwise. -strategy = DefaultVideoStrategy.atMost(1000).build(); - -// Ensures that minor and major dimension are at most the given values - scales down otherwise. -strategy = DefaultVideoStrategy.atMost(500, 1000).build(); -``` - -In fact, all of these will simply call `new DefaultVideoStrategy.Builder(resizer)` with a special -resizer. We offer handy resizers: - -|Name|Description| -|----|-----------| -|`ExactResizer`|Returns the exact dimensions passed to the constructor.| -|`AspectRatioResizer`|Crops the input size to match the given aspect ratio.| -|`FractionResizer`|Reduces the input size by the given fraction (0..1).| -|`AtMostResizer`|If needed, reduces the input size so that the "at most" constraints are matched. Aspect ratio is kept.| -|`PassThroughResizer`|Returns the input size unchanged.| - -You can also group resizers through `MultiResizer`, which applies resizers in chain: - -```java -// First scales down, then ensures size is at most 1000. Order matters! -Resizer resizer = new MultiResizer(); -resizer.addResizer(new FractionResizer(0.5F)); -resizer.addResizer(new AtMostResizer(1000)); - -// First makes it 16:9, then ensures size is at most 1000. Order matters! -Resizer resizer = new MultiResizer(); -resizer.addResizer(new AspectRatioResizer(16F / 9F)); -resizer.addResizer(new AtMostResizer(1000)); -``` - -This option is already available through the DefaultVideoStrategy builder, so you can do: - -```java -DefaultVideoStrategy strategy = new DefaultVideoStrategy.Builder() - .addResizer(new AspectRatioResizer(16F / 9F)) - .addResizer(new FractionResizer(0.5F)) - .addResizer(new AtMostResizer(1000)) - .build(); -``` - -##### Other options - -You can configure the `DefaultVideoStrategy` with other options unrelated to the video size: - -```java -DefaultVideoStrategy strategy = new DefaultVideoStrategy.Builder() - .bitRate(bitRate) - .bitRate(DefaultVideoStrategy.BITRATE_UNKNOWN) // tries to estimate - .frameRate(frameRate) // will be capped to the input frameRate - .keyFrameInterval(interval) // interval between key-frames in seconds - .build(); -``` - -### Compatibility - -As stated pretty much everywhere, **not all codecs/devices/manufacturers support all sizes/options**. -This is a complex issue which is especially important for video strategies, as a wrong size can lead -to a transcoding error or corrupted file. - -Android platform specifies requirements for manufacturers through the [CTS (Compatibility test suite)](https://source.android.com/compatibility/cts). -Only a few codecs and sizes are **strictly** required to work. - -We collect common presets in the `DefaultVideoStrategies` class: - -```java -Transcoder.into(filePath) - .setVideoTrackStrategy(DefaultVideoStrategies.for720x1280()) // 16:9 - .setVideoTrackStrategy(DefaultVideoStrategies.for360x480()) // 4:3 - // ... -``` \ No newline at end of file +Migrated to https://opensource.deepmedia.io/transcoder/track-strategies diff --git a/docs-legacy/_docs/validators.md b/docs-legacy/_docs/validators.md index 1692554e..1228ce17 100644 --- a/docs-legacy/_docs/validators.md +++ b/docs-legacy/_docs/validators.md @@ -2,53 +2,8 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder/validators title: "Validators" -description: "Validate or abort the transcoding process" order: 5 -disqus: 1 --- -Validators tell the engine whether the transcoding process should start or not based on the status -of the audio and video track. - -```java -Transcoder.into(filePath) - .setValidator(validator) - // ... -``` - -This can be used, for example, to: - -- avoid transcoding when video resolution is already OK with our needs -- avoid operating on files without an audio/video stream -- avoid operating on files with an audio/video stream - -Validators should implement the `validate(TrackStatus, TrackStatus)` and inspect the status for video -and audio tracks. When `false` is returned, transcoding will complete with the `SUCCESS_NOT_NEEDED` status code. -The TrackStatus enum contains the following values: - -|Value|Meaning| -|-----|-------| -|`TrackStatus.ABSENT`|This track was absent in the source file.| -|`TrackStatus.PASS_THROUGH`|This track is about to be copied as-is in the target file.| -|`TrackStatus.COMPRESSING`|This track is about to be processed and compressed in the target file.| -|`TrackStatus.REMOVING`|This track will be removed in the target file.| - -The `TrackStatus` value depends on the [track strategy](track-strategies) that was used. -We provide a few validators that can be injected for typical usage. - -##### DefaultValidator - -This is the default validator and it returns true when any of the track is `COMPRESSING` or `REMOVING`. -In the other cases, transcoding is typically not needed so we abort the operation. - -##### WriteAlwaysValidator - -This validator always returns true and as such will always write to target file, no matter the track status, -presence of tracks and so on. For instance, the output container file might have no tracks. - -##### WriteVideoValidator - -A Validator that gives priority to the video track. Transcoding will not happen if the video track does not need it, -even if the audio track might need it. If reducing file size is your only concern, this can avoid compressing -files that would not benefit so much from compressing the audio track only. +Migrated to https://opensource.deepmedia.io/transcoder/validators diff --git a/docs-legacy/_extra/contact.md b/docs-legacy/_extra/contact.md index 6aa9334f..683854eb 100644 --- a/docs-legacy/_extra/contact.md +++ b/docs-legacy/_extra/contact.md @@ -2,12 +2,6 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder title: "Contact" -order: 3 --- -This library is maintained by [Mattia Iavarone](https://github.com/natario1) (@natario1). -Feel free to contact me privately by sending an email, -for support, consulting, or have any other business-related question. - -To report issues, please use the [project GitHub page](https://github.com/natario1/Transcoder). - +Migrated to https://opensource.deepmedia.io/transcoder diff --git a/docs-legacy/_extra/contributing.md b/docs-legacy/_extra/contributing.md index 307a4f98..c4c64357 100644 --- a/docs-legacy/_extra/contributing.md +++ b/docs-legacy/_extra/contributing.md @@ -2,60 +2,6 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder title: "Contributing & License" -order: 1 --- -Everyone is welcome to contribute with suggestions or pull requests, as the library is under active development. - -We are grateful to anyone who will contribute with fixes, features or feature requests. If you don't -want to get involved but still want to support the project, please [consider donating](donate). - -### Bug reports - -Please make sure to fill the bug report issue template on GitHub, if applicable. -We highly recommend to try to reproduce the bug in the demo app, as this helps a lot in debugging -and excludes programming errors from your side. - -Make sure to include: - -- A clear and concise description of what the bug is -- Transcoder version, device type, Android API level -- Exact steps to reproduce the issue -- Description of the expected behavior -- The original media file(s) that manifest the problem - -Recommended extras: - -- LogCat logs (use `Logger.setLogLevel(LEVEL_VERBOSE)` to print all) -- Link to a GitHub repo where the bug is reproducible - -### Pull Requests - -Please open an issue first! - -Unless your PR is a simple fix (typos, documentation, bugs with obvious solution), opening an issue -will let us discuss the problem, take design decisions and have a reference to the issue description. - -If you can, please write tests. We are planning to work on improving the library test coverage soon. - -### License - -This project is licensed under Apache 2.0. It consists of improvements over -the [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder) -project which was licensed under Apache 2.0 as well: - -``` -Copyright (C) 2014-2016 Yuya Tanaka - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -``` +Migrated to https://opensource.deepmedia.io/transcoder diff --git a/docs-legacy/_extra/donate.md b/docs-legacy/_extra/donate.md index 68d25066..dbc95dfd 100644 --- a/docs-legacy/_extra/donate.md +++ b/docs-legacy/_extra/donate.md @@ -2,16 +2,6 @@ layout: redirect redirect_to: https://opensource.deepmedia.io/transcoder title: "Donate" -order: 2 --- -Transcoder is maintained and, for the most part, developed by Mattia Iavarone ([contact me!](contact)). If you like the project, -use it with profit, or simply want to thank back, please consider -[sponsoring me](https://github.com/sponsors/natario1) through the GitHub Sponsors program! - -I offer private support hours through the sponsorship program and I'm open to help your -company with features that are not currently supported by the open source project. - -Thank you for any contribution! - - +Migrated to https://opensource.deepmedia.io/transcoder diff --git a/docs-legacy/home.md b/docs-legacy/home.md index 9b95afd5..7ddf3f2c 100644 --- a/docs-legacy/home.md +++ b/docs-legacy/home.md @@ -4,47 +4,4 @@ redirect_to: https://opensource.deepmedia.io/transcoder title: "Transcoder" --- -# Transcoder - -Transcoder is a well documented Android library providing hardware-accelerated video transcoding, -using MediaCodec APIs instead of native code (no FFMPEG patent issues). - -

- -

- -- Fast transcoding to AAC/AVC -- Hardware accelerated -- Multithreaded -- Convenient, fluent API -- Concatenate multiple video and audio tracks [[docs]](docs/concatenation) -- Clip or trim video segments [[docs]](docs/clipping) -- Choose output size, with automatic cropping [[docs]](docs/track-strategies#video-size) -- Choose output rotation [[docs]](docs/advanced-options#video-rotation) -- Choose output speed [[docs]](docs/advanced-options#video-speed) -- Choose output frame rate [[docs]](docs/track-strategies#other-options) -- Choose output audio channels [[docs]](docs/track-strategies#audio-strategies) -- Choose output audio sample rate [[docs]](docs/track-strategies#audio-strategies) -- Override frames timestamp, e.g. to slow down the middle part of the video [[docs]](docs/advanced-options#time-interpolation) -- Error handling [[docs]](docs/events) -- Configurable validators to e.g. avoid transcoding if the source is already compressed enough [[docs]](docs/validators) -- Configurable video and audio strategies [[docs]](docs/track-strategies) - -### Get started - -Get started with [install info](about/install), [quick setup](about/getting-started), or -start reading the in-depth [documentation](docs/data-sources). - -### Notes - -This project started as a fork of [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder). -With respect to the source project, which misses most of the functionality listed above, -we have also fixed a huge number of bugs and are much less conservative when choosing options -that might not be supported. The source project will always throw - for example, accepting only 16:9, -AVC Baseline Profile videos - we prefer to try and let the codec fail if it wants to. - -### Support - -If you like the project, use it with profit, and want to thank back, please consider [donating or -becoming a supporter](extra/donate). - +Migrated to https://opensource.deepmedia.io/transcoder \ No newline at end of file diff --git a/docs-legacy/index.md b/docs-legacy/index.md index 82d0f3bf..ee7fbb4e 100644 --- a/docs-legacy/index.md +++ b/docs-legacy/index.md @@ -4,4 +4,4 @@ redirect_to: https://opensource.deepmedia.io/transcoder title: "Transcoder" --- -Accelerated video transcoding on Android using MediaCodec APIs, without native code (no FFMPEG patent issues). Supports cropping to any dimension, concatenation, audio processing and much more. \ No newline at end of file +Migrated to https://opensource.deepmedia.io/transcoder diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..995fe576 --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ +Read the docs at https://opensource.deepmedia.io/transcoder. diff --git a/docs/advanced-options.mdx b/docs/advanced-options.mdx new file mode 100644 index 00000000..0da292d4 --- /dev/null +++ b/docs/advanced-options.mdx @@ -0,0 +1,101 @@ +--- +title: Advanced Options +--- + +# Advanced Options + +## Video rotation + +You can set the output video rotation with the `setRotation(int)` method. This will apply a clockwise +rotation to the input video frames. Accepted values are `0`, `90`, `180`, `270`: + +```kotlin +Transcoder.into(filePath) + .setVideoRotation(rotation) // 0, 90, 180, 270 + // ... +``` + +## Time interpolation + +We offer APIs to change the timestamp of each video and audio frame. You can pass a `TimeInterpolator` +to the transcoder builder to be able to receive the frame timestamp as input, and return a new one +as output. + +```kotlin +Transcoder.into(filePath) + .setTimeInterpolator(timeInterpolator) + // ... +``` + +As an example, this is the implementation of the default interpolator, called `DefaultTimeInterpolator`, +that will just return the input time unchanged: + +```kotlin +override fun interpolate(type: TrackType, time: Long): Long { + // Receive input time in microseconds and return a possibly different one. + return time +} +``` + +It should be obvious that returning invalid times can make the process crash at any point, or at least +the transcoding operation fail. + +## Video speed + +We also offer a special time interpolator called `SpeedTimeInterpolator` that accepts a `float` parameter +and will modify the video speed. + +- A speed factor equal to 1 will leave speed unchanged +- A speed factor < 1 will slow the video down +- A speed factor > 1 will accelerate the video + +This interpolator can be set using `setTimeInterpolator(TimeInterpolator)`, or, as a shorthand, +using `setSpeed(float)`: + +```kotlin +Transcoder.into(filePath) + .setSpeed(0.5F) // 0.5x + .setSpeed(1F) // Unchanged + .setSpeed(2F) // Twice as fast + // ... +``` + +## Audio stretching + +When a time interpolator alters the frames and samples timestamps, you can either remove audio or +stretch the audio samples to the new length. This is done through the `AudioStretcher` interface: + +```kotlin +Transcoder.into(filePath) + .setAudioStretcher(audioStretcher) + // ... +``` + +The default audio stretcher, `DefaultAudioStretcher`, will: + +- When we need to shrink a group of samples, cut the last ones +- When we need to stretch a group of samples, insert noise samples in between + +Please take a look at the implementation and read class documentation. + +## Audio resampling + +When a sample rate different than the input is specified (by the `TrackStrategy`, or, when using the +default audio strategy, by `DefaultAudioStrategy.Builder.sampleRate()`), this library will automatically +perform sample rate conversion for you. + +This operation is performed by a class called `AudioResampler`. We offer the option to pass your +own resamplers through the transcoder builder: + +```kotlin +Transcoder.into(filePath) + .setAudioResampler(audioResampler) + // ... +``` + +The default audio resampler, `DefaultAudioResampler`, will perform both upsampling and downsampling +with very basic algorithms (drop samples when downsampling, repeat samples when upsampling). +Upsampling is generally discouraged - implementing a real upsampling algorithm is probably out of +the scope of this library. + +Please take a look at the implementation and read class documentation. \ No newline at end of file diff --git a/docs/changelog.mdx b/docs/changelog.mdx new file mode 100644 index 00000000..6b4dc3fe --- /dev/null +++ b/docs/changelog.mdx @@ -0,0 +1,193 @@ +--- +title: Changelog +--- + +# Changelog + +New versions are released through GitHub, so the reference page is the [GitHub Releases](https://github.com/deepmedia/Transcoder/releases) page. + +## 0.10.X + +### 0.10.5 + +- Fix: Honor buffer.hasRemaining() and otherwise release the buffer. ([#182][182]) +- Fix: Call progress even when not advanced. ([#170][170]) +- Fix: FilePathDataSource crash before initialize. ([#160][160]) + +[Compare 0.10.4...0.10.5](https://github.com/deepmedia/Transcoder/compare/v0.10.4...v0.10.5). + +### 0.10.4 + +- Fix: fixed crash in specific conditions ([#140][140]) +- New: AssetFileDescriptorDataSource, can be used to transcode from AssetFileDescriptors ([#140][140]) + +[Compare 0.10.3...0.10.4](https://github.com/deepmedia/Transcoder/compare/v0.10.3...v0.10.4). + +### 0.10.3 + +- Fix: error when merging many files, thanks to [@DamonChen117][DamonChen117] ([#134][134]) +- Fix: more concatenation issues + +[Compare 0.10.2...0.10.3](https://github.com/deepmedia/Transcoder/compare/v0.10.2...v0.10.3). + +### 0.10.2 + +- Fix: error when merging many files ([#132][132]) + +[Compare 0.10.1...0.10.2](https://github.com/deepmedia/Transcoder/compare/v0.10.1...v0.10.2). + +### 0.10.1 + +- Fix: thumbnails upside down ([#125][125]) +- Fix: clip/trim issues ([#127][127]) +- Fix: seeking issues ([#128][128]) +- Fix: concatenation failure ([#130][130]) + +[Compare 0.10.0...0.10.1](https://github.com/deepmedia/Transcoder/compare/v0.10.0...v0.10.1). + +### 0.10.0 + +> Transcoder is now distributed through Maven Central. Snapshot releases are available as well. + +- New: thumbnails support ([#119][119]) +- Improvement: rewritten transcoding pipeline ([#118][118]) +- Fix: many bugs fixed while rewriting the pipeline ([#118][118]) + +[Compare 0.9.1...0.10.0](https://github.com/deepmedia/Transcoder/compare/v0.9.1...v0.10.0). + +## 0.9.X + +### 0.9.1 + +- Improvement: `DefaultDataSink` new constructor with support for FileDescriptor. ([#87][87]) + +[Compare 0.9.0...0.9.1](https://github.com/deepmedia/Transcoder/compare/v0.9.0...v0.9.1). + +### 0.9.0 + +- New: `BlankAudioDataSource` can be used to add muted audio to a video-only track, thanks to [@mudar][mudar] ([#64][64]) +- Enhancement: you can now concatenate multiple files even if some of them have no audio, thanks to [@mudar][mudar] ([#64][64]) +- Enhancement: you can now concatenate multiple files without audio track, thanks to [@cbernier2][cbernier2] ([#61][61]) + +[Compare 0.8.0...0.9.0](https://github.com/deepmedia/Transcoder/compare/v0.8.0...v0.9.0). + +## 0.8.X + +### 0.8.0 + +- New: `TrimDataSource` to trim segments. Use it to wrap your original source. Thanks to [@mudar][mudar] ([#50][50]) +- New: `ClipDataSource`, just likes `TrimDataSource` but selects trim values with respect to video start ([#54][54]) + +> Transcoder will trim video segments only at the closest video sync frame. If your video has few sync +frames, the trim timestamp might be different than what was selected. + +[Compare 0.7.4...0.8.0](https://github.com/deepmedia/Transcoder/compare/v0.7.4...v0.8.0). + +## Older versions + +### 0.7.4 + +- Fix: fixed Xamarin incompatibility, thanks to [@aweck][aweck] ([#41][41]) +- Fix: fixed small bugs with specific API versions / media files ([#47][47]) +- Fix: fixed issues with specific media files, ensure consistent onProgress callback ([#48][48]) + +[Compare 0.7.3...0.7.4](https://github.com/deepmedia/Transcoder/compare/v0.7.3...v0.7.4). + +### 0.7.3 + +- Fix: fixed bug with files that do not have an audio track, thanks to [@pawegio][pawegio] ([#31][31]) +- Fix: fixed possible issues with FilePathDataSource ([#32][32]) + +[Compare 0.7.2...0.7.3](https://github.com/deepmedia/Transcoder/compare/v0.7.2...v0.7.3). + +### 0.7.2 + +- Improvement: better input format detection. Fixes bugs with certain files ([#29][29]) +- Improvement: added `DefaultAudioStrategy.Builder.bitRate()` option ([#29][29]) + +[Compare 0.7.1...0.7.2](https://github.com/deepmedia/Transcoder/compare/v0.7.1...v0.7.2). + +### 0.7.1 + +- Improvement: update the underlying OpenGL library ([#20][20]) + +[Compare 0.7.0...0.7.1](https://github.com/deepmedia/Transcoder/compare/v0.7.0...v0.7.1). + +### 0.7.0 + +- New: video concatenation to stitch together multiple media ([#14][14]) +- New: select a specific track type (`VIDEO` or `AUDIO`) for sources ([#14][14]) +- New: audio resampling through `DefaultAudioStrategy` ([#16][16]) +- New: custom resampling through `TranscoderOptions.setAudioResampler()` ([#16][16]) +- Breaking change: `TranscoderOptions.setDataSource()` renamed to `addDataSource()` ([#14][14]) +- Breaking change: `TranscoderOptions.setRotation()` renamed to `setVideoRotation()` ([#14][14]) +- Breaking change: `DefaultVideoStrategy.iFrameInterval()` renamed to `keyFrameInterval()` ([#14][14]) +- Breaking change: `DefaultAudioStrategy` now uses a builder - removed old constructor ([#16][16]) +- Improvement: rotate videos through OpenGL instead of using metadata ([#14][14]) +- Improvement: when concatenating multiple sources, automatically clip the longer track (audio or video) ([#17][17]) +- Improvement: various bug fixed ([#18][18]) + +[Compare 0.6.0...0.7.0](https://github.com/deepmedia/Transcoder/compare/v0.6.0...v0.7.0). + +### 0.6.0 + +- New: ability to change video/audio speed and change each frame timestamp ([#10][10]) +- New: ability to set the video output rotation ([#8][8]) +- Improvement: new frame dropping algorithm, thanks to [@Saqrag][Saqrag] ([#9][9]) +- Improvement: avoid format validation on tracks coming from PassThroughTrackTranscoder, thanks to [@Saqrag][Saqrag] ([#11][11]) + +[Compare 0.5.0...0.6.0](https://github.com/deepmedia/Transcoder/compare/v0.5.0...v0.6.0). + +### 0.5.0 + +- New: video cropping to any dimension. Encoder will crop the exceeding size. ([#6][6]) +- New: `AspectRatioResizer` to crop to a given aspect ratio. ([#6][6]) +- Breaking change: `MediaTranscoder` renamed to `Transcoder`. ([#6][6]) +- Breaking change: `MediaTranscoderOptions` renamed to `TranscoderOptions`. ([#6][6]) +- Breaking change: `MediaTranscoder.Listener` renamed to `TranscoderListener`. ([#6][6]) +- Improvement: use [EglCore](https://github.com/natario1/EglCore) to replace GL logic. ([#5][5]) +- Improvement: bug fixes and a new demo app to test transcoding options easily ([#4][4]) + +[Saqrag]: https://github.com/Saqrag +[pawegio]: https://github.com/pawegio +[aweck]: https://github.com/aweck +[mudar]: https://github.com/mudar +[cbernier2]: https://github.com/cbernier2 +[DamonChen117]: https://github.com/DamonChen117 + +[4]: https://github.com/deepmedia/Transcoder/pull/4 +[5]: https://github.com/deepmedia/Transcoder/pull/5 +[6]: https://github.com/deepmedia/Transcoder/pull/6 +[8]: https://github.com/deepmedia/Transcoder/pull/8 +[9]: https://github.com/deepmedia/Transcoder/pull/9 +[10]: https://github.com/deepmedia/Transcoder/pull/10 +[11]: https://github.com/deepmedia/Transcoder/pull/11 +[14]: https://github.com/deepmedia/Transcoder/pull/14 +[16]: https://github.com/deepmedia/Transcoder/pull/16 +[17]: https://github.com/deepmedia/Transcoder/pull/17 +[18]: https://github.com/deepmedia/Transcoder/pull/18 +[20]: https://github.com/deepmedia/Transcoder/pull/20 +[29]: https://github.com/deepmedia/Transcoder/pull/29 +[31]: https://github.com/deepmedia/Transcoder/pull/31 +[32]: https://github.com/deepmedia/Transcoder/pull/32 +[41]: https://github.com/deepmedia/Transcoder/pull/41 +[47]: https://github.com/deepmedia/Transcoder/pull/47 +[48]: https://github.com/deepmedia/Transcoder/pull/48 +[50]: https://github.com/deepmedia/Transcoder/pull/50 +[54]: https://github.com/deepmedia/Transcoder/pull/54 +[61]: https://github.com/deepmedia/Transcoder/pull/61 +[64]: https://github.com/deepmedia/Transcoder/pull/64 +[87]: https://github.com/deepmedia/Transcoder/pull/87 +[118]: https://github.com/deepmedia/Transcoder/pull/118 +[119]: https://github.com/deepmedia/Transcoder/pull/119 +[125]: https://github.com/deepmedia/Transcoder/pull/125 +[127]: https://github.com/deepmedia/Transcoder/pull/127 +[128]: https://github.com/deepmedia/Transcoder/pull/128 +[130]: https://github.com/deepmedia/Transcoder/pull/130 +[132]: https://github.com/deepmedia/Transcoder/pull/132 +[134]: https://github.com/deepmedia/Transcoder/pull/134 +[140]: https://github.com/deepmedia/Transcoder/pull/140 +[160]: https://github.com/deepmedia/Transcoder/pull/160 +[170]: https://github.com/deepmedia/Transcoder/pull/170 +[182]: https://github.com/deepmedia/Transcoder/pull/182 + diff --git a/docs/clipping.mdx b/docs/clipping.mdx new file mode 100644 index 00000000..d2fa03c7 --- /dev/null +++ b/docs/clipping.mdx @@ -0,0 +1,56 @@ +--- +title: Clip & Trim +--- + +# Clip & Trim + +Starting from `v0.8.0`, Transcoder offers the option to clip or trim video segments, on one or +both ends. This is done by using special `DataSource` objects that wrap you original source, +so that, in case of [concatenation](concatenation) of multiple media files, the trimming values +can be set individually for each segment. + +> If Transcoder determines that the video should be decoded and re-encoded (status is `TrackStatus.COMPRESSING`) +the clipping position is respected precisely. However, if your [strategy](track-strategies) does not +include video decoding / re-encoding, the clipping position will be moved to the closest video sync frame. +This means that the clipped output duration might be different than expected, +depending on the frequency of sync frames in your original file. + +## TrimDataSource + +The `TrimDataSource` class lets you trim segments by specifying the amount of time to be trimmed +at both ends. For example, the code below will trim the file by 1 second at the beginning, and +2 seconds at the end: + +```kotlin +let source: DataSource = UriDataSource(context, uri) +let trim: DataSource = TrimDataSource(source, 1000 * 1000, 2 * 1000 * 1000) +Transcoder.into(filePath) + .addDataSource(trim) + .transcode() +``` + +It is recommended to always check `source.getDurationUs()` to compute the correct values. + +## ClipDataSource + +The `ClipDataSource` class lets you clip segments by specifying a time window. For example, +the code below clip the file from second 1 until second 5: + +```kotlin +let source: DataSource = UriDataSource(context, uri) +let clip: DataSource = ClipDataSource(source, 1000 * 1000, 5 * 1000 * 1000) +Transcoder.into(filePath) + .addDataSource(clip) + .transcode() +``` + +It is recommended to always check `source.getDurationUs()` to compute the correct values. + +## Related APIs + +|Method|Description| +|------|-----------| +|`TrimDataSource(source, long)`|Creates a new data source trimmed on start.| +|`TrimDataSource(source, long, long)`|Creates a new data source trimmed on both ends.| +|`ClipDataSource(source, long)`|Creates a new data source clipped on start.| +|`ClipDataSource(source, long, long)`|Creates a new data source clipped on both ends.| \ No newline at end of file diff --git a/docs/concatenation.mdx b/docs/concatenation.mdx new file mode 100644 index 00000000..559cb38d --- /dev/null +++ b/docs/concatenation.mdx @@ -0,0 +1,60 @@ +--- +title: Concatenation +--- + +# Concatenation + +## Appending sources + +As you might have guessed from the previous section, you can use `addDataSource(source)` multiple times. +All the source files will be stitched together: + +```kotlin +Transcoder.into(filePath) + .addDataSource(source1) + .addDataSource(source2) + .addDataSource(source3) + // ... +``` + +In the above example, the three videos will be stitched together in the order they are added +to the builder. Once `source1` ends, we'll append `source2` and so on. The library will take care +of applying consistent parameters (frame rate, bit rate, sample rate) during the conversion. + +This is a powerful tool since it can be used per-track: + +```kotlin +Transcoder.into(filePath) + .addDataSource(source1) // Audio & Video, 20 seconds + .addDataSource(TrackType.VIDEO, source2) // Video, 5 seconds + .addDataSource(TrackType.VIDEO, source3) // Video, 5 seconds + .addDataSource(TrackType.AUDIO, source4) // Audio, 10 sceonds + // ... +``` + +In the above example, the output file will be 30 seconds long: + +``` +Video: | •••••••••••••••••• source1 •••••••••••••••••• | •••• source2 •••• | •••• source3 •••• | +Audio: | •••••••••••••••••• source1 •••••••••••••••••• | •••••••••••••• source4 •••••••••••••• | +``` + +And that's all you need to do. + +## Automatic clipping + +When concatenating data from multiple sources and on different tracks, it's common to have +a total audio length that is different than the total video length. + +In this case, `Transcoder` will automatically clip the longest track to match the shorter. +For example: + +```kotlin +Transcoder.into(filePath) + .addDataSource(TrackType.VIDEO, video1) // Video, 30 seconds + .addDataSource(TrackType.VIDEO, video2) // Video, 30 seconds + .addDataSource(TrackType.AUDIO, music) // Audio, 3 minutes + // ... +``` + +In the situation above, we won't use the full music track, but only the first minute of it. \ No newline at end of file diff --git a/docs/data-sources.mdx b/docs/data-sources.mdx new file mode 100644 index 00000000..a32b6146 --- /dev/null +++ b/docs/data-sources.mdx @@ -0,0 +1,60 @@ +--- +title: Data Sources +--- + +# Data Sources + +Starting a transcoding operation will require a source for our data, which is not necessarily +a `File`. The `DataSource` objects will automatically take care about releasing streams / resources, +which is convenient but it means that they can not be used twice. + +```kotlin +Transcoder.into(filePath) + .addDataSource(source1) + .transcode() +``` + +## Source Types + +#### UriDataSource + +The Android friendly source can be created with `UriDataSource(context, uri)` or simply +using `addDataSource(context, uri)` in the transcoding builder. + +#### FileDescriptorDataSource + +A data source backed by a file descriptor. Use `FileDescriptorDataSource(descriptor)` or +simply `addDataSource(descriptor)` in the transcoding builder. Note that it is the caller +responsibility to close the file descriptor. + +#### FilePathDataSource + +A data source backed by a file absolute path. Use `FilePathDataSource(path)` or +simply `addDataSource(path)` in the transcoding builder. + +#### AssetFileDescriptorDataSource + +A data source backed by Android's AssetFileDescriptor. Use `AssetFileDescriptorDataSource(descriptor)` +or simply `addDataSource(descriptor)` in the transcoding builder. Note that it is the caller +responsibility to close the file descriptor. + +## Track specific sources + +Although a media source can have both audio and video, you can select a specific track +for transcoding and exclude the other(s). For example, to select the video track only: + +```java +Transcoder.into(filePath) + .addDataSource(TrackType.VIDEO, source) + .transcode() +``` + +## Related APIs + +|Method|Description| +|------|-----------| +|`addDataSource(Context, Uri)`|Adds a new source for the given Uri.| +|`addDataSource(FileDescriptor)`|Adds a new source for the given FileDescriptor.| +|`addDataSource(String)`|Adds a new source for the given file path.| +|`addDataSource(DataSource)`|Adds a new source.| +|`addDataSource(TrackType, DataSource)`|Adds a new source restricted to the given TrackType.| \ No newline at end of file diff --git a/docs/events.mdx b/docs/events.mdx new file mode 100644 index 00000000..995a7715 --- /dev/null +++ b/docs/events.mdx @@ -0,0 +1,55 @@ +--- +title: Events +--- + +# Events + +Transcoding will happen on a background thread, but we will send updates through the `TranscoderListener` +interface, which can be applied when building the request: + +```kotlin +Transcoder.into(filePath) + .setListenerHandler(handler) + .setListener(object: TranscoderListener { + override fun onTranscodeProgress(progress: Double) = Unit + override fun onTranscodeCompleted(successCode: Int) = Unit + override fun onTranscodeCanceled() = Unit + override fun onTranscodeFailed(exception: Throwable) = Unit + }) + // ... +``` + +All of the listener callbacks are called: + +- If present, on the handler specified by `setListenerHandler()` +- If it has a handler, on the thread that started the `transcode()` call +- As a last resort, on the UI thread + +### onTranscodeProgress + +This simply sends a double indicating the current progress. The value is typically between 0 and 1, +but can be a negative value to indicate that we are not able to compute progress (yet?). + +This is the right place to update a ProgressBar, for example. + +### onTranscodeCanceled + +The transcoding operation was canceled. This can happen when the `Future` returned by `transcode()` +is cancelled by the user. + +### onTranscodeFailed + +This can happen in a number of cases and is typically out of our control. Input options might be +wrong, write permissions might be missing, codec might be absent, input file might be not supported +or simply corrupted. + +You can take a look at the `Throwable` being passed to know more about the exception. + +### onTranscodeCompleted + +Transcoding operation did succeed. The success code can be: + +|Code|Meaning| +|----|-------| +|`Transcoder.SUCCESS_TRANSCODED`|Transcoding was executed successfully. Transcoded file was written to the output path.| +|`Transcoder.SUCCESS_NOT_NEEDED`|Transcoding was not executed because it was considered **not needed** by the `Validator`.| diff --git a/docs/index.mdx b/docs/index.mdx new file mode 100644 index 00000000..baa72588 --- /dev/null +++ b/docs/index.mdx @@ -0,0 +1,81 @@ +--- +title: Intro +docs: + - install + - changelog + - data-sources + - clipping + - concatenation + - events + - validators + - track-strategies + - advanced-options +--- + +# Intro + +The Transcoder library transcodes and compresses video files into the MP4 format, with audio support, using hardware-accelerated +Android codecs available on the device. Works on API 18+ and supports the following set of features: + +- Fast transcoding to AAC/AVC +- Hardware accelerated +- Convenient, fluent API +- Thumbnails support +- [Concatenate](concatenation) multiple video and audio tracks +- [Clip or trim](clipping) video segments +- Configure [output size](track-strategies#video-size), with automatic cropping +- Configure [output rotation](advanced-options#video-rotation) +- Configure [output speed](advanced-options#video-speed) +- Configure [output frame rate](track-strategies#other-options) +- Configure [output audio channels](track-strategies#audio-strategies) and sample rate +- [Override timestamp](advanced-options#time-interpolation) of frames, for example to slow down parts of the video +- [Error handling](events) +- Configurable [validators](validators) to e.g. avoid transcoding if the source is already compressed enough +- Configurable video and audio [strategies](track-strategies) + +> This project started as a fork of [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder). +With respect to the source project, which misses most of the functionality listed above, +we have also fixed a huge number of bugs and are much less conservative when choosing options +that might not be supported. The source project will always throw - for example, accepting only 16:9, +AVC Baseline Profile videos - we prefer to try and let the codec fail if it wants to. + +## Minimal example + +```kotlin +Transcoder.into(filePath) + .addDataSource(context, uri) // or... + .addDataSource(filePath) // or... + .addDataSource(fileDescriptor) // or... + .addDataSource(dataSource) + .setListener(object : TranscoderListener { + override fun onTranscodeProgress(progress: Double) = Unit + override fun onTranscodeCompleted(successCode: Int) = Unit + override fun onTranscodeCanceled() = Unit + override fun onTranscodeFailed(exception: Throwable) = Unit + }).transcode() +``` + +Please keep reading the documentation to learn about [install instructions](install), configuration options and APIs. + + +## License + +This project is licensed under Apache 2.0. It consists of improvements over +the [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder) +project which was licensed under Apache 2.0 as well: + +``` +Copyright (C) 2014-2016 Yuya Tanaka + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` \ No newline at end of file diff --git a/docs/install.mdx b/docs/install.mdx new file mode 100644 index 00000000..14cbf649 --- /dev/null +++ b/docs/install.mdx @@ -0,0 +1,37 @@ +--- +title: Install +--- + +# Installation + +Transcoder is publicly hosted on the [Maven Central](https://repo1.maven.org/maven2/com/otaliastudios/) +repository, where you can download the AAR package. To fetch with Gradle, assuming that `mavenCentral()` is already +one of your repository sources, simply declare a new dependency: + +```kotlin +dependencies { + api("com.otaliastudios:transcoder:LATEST_VERSION") +} +``` + +Replace `LATEST_VERSION` with the latest version number, {version}. + +## Snapshots + +We regularly push development snapshots of the library at `https://s01.oss.sonatype.org/content/repositories/snapshots/` +on each push to main. To use snapshots, add the url as a maven repository and depend on `latest-SNAPSHOT`: + +```kotlin +// settings.gradle.kts +pluginManagement { + repositories { + gradlePluginPortal() + maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") + } +} + +// build.gradle.kts +dependencies { + implementation("com.otaliastudios:transcoder:latest-SNAPSHOT") +} +``` \ No newline at end of file diff --git a/docs/track-strategies.mdx b/docs/track-strategies.mdx new file mode 100644 index 00000000..d9e24716 --- /dev/null +++ b/docs/track-strategies.mdx @@ -0,0 +1,153 @@ +--- +title: Track Strategies +--- + +# Track Strategies + +Track strategies return options for each track (audio or video) for the engine to understand **how** +and **if** this track should be transcoded, and whether the whole process should be aborted. + +```kotlin +Transcoder.into(filePath) + .setVideoTrackStrategy(videoStrategy) + .setAudioTrackStrategy(audioStrategy) + // ... +``` + +The point of `TrackStrategy` is to inspect the input `android.media.MediaFormat` and return +the output `android.media.MediaFormat`, filled with required options. + +## Basic Strategies + +This library offers track specific strategies that help with audio and video options (see +[Audio Strategies](#audio-strategies) and [Video Strategies](#video-strategies)). +In addition, we have a few built-in strategies that can work for both audio and video: + +### PassThroughTrackStrategy + +A TrackStrategy that asks the encoder to keep this track as is, by returning the same input +format. Note that this is risky, as the input track format might not be supported my the MP4 container. + +This will set the `TrackStatus` to `TrackStatus.PASS_THROUGH`. + +### RemoveTrackStrategy + +A TrackStrategy that asks the encoder to remove this track from the output container, by returning null. +For instance, this can be used as an audio strategy to remove audio from video/audio streams. + +This will set the `TrackStatus` to `TrackStatus.REMOVING`. + +## Audio Strategies + +The default internal strategy for audio is a `DefaultAudioStrategy`, which converts the +audio stream to AAC format with the specified number of channels and [sample rate](advanced-options). + +```kotlin +val strategy: DefaultAudioStrategy = DefaultAudioStrategy.builder() + .channels(DefaultAudioStrategy.CHANNELS_AS_INPUT) + .channels(1) + .channels(2) + .sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT) + .sampleRate(44100) + .sampleRate(30000) + .bitRate(DefaultAudioStrategy.BITRATE_UNKNOWN) + .bitRate(bitRate) + .build() + +Transcoder.into(filePath) + .setAudioTrackStrategy(strategy) + // ... +``` + +Take a look at the source code to understand how to manage the `android.media.MediaFormat` object. + +## Video Strategies + +The default internal strategy for video is a `DefaultVideoStrategy`, which converts the +video stream to AVC format and is very configurable. The class helps in defining an output size. +If the output size does not match the aspect ratio of the input stream size, `Transcoder` will +crop part of the input so it matches the final ratio. + +### Video Size + +We provide helpers for common tasks: + +```kotlin +// Sets an exact size. If aspect ratio does not match, cropping will take place. +val strategy = DefaultVideoStrategy.exact(1080, 720).build() + +// Keeps the aspect ratio, but scales down the input size with the given fraction. +val strategy = DefaultVideoStrategy.fraction(0.5F).build() + +// Ensures that each video size is at most the given value - scales down otherwise. +val strategy = DefaultVideoStrategy.atMost(1000).build() + +// Ensures that minor and major dimension are at most the given values - scales down otherwise. +val strategy = DefaultVideoStrategy.atMost(500, 1000).build() +``` + +In fact, all of these will simply call `DefaultVideoStrategy.Builder(resizer)` with a special +resizer. We offer handy resizers: + +|Name|Description| +|----|-----------| +|`ExactResizer`|Returns the exact dimensions passed to the constructor.| +|`AspectRatioResizer`|Crops the input size to match the given aspect ratio.| +|`FractionResizer`|Reduces the input size by the given fraction (0..1).| +|`AtMostResizer`|If needed, reduces the input size so that the "at most" constraints are matched. Aspect ratio is kept.| +|`PassThroughResizer`|Returns the input size unchanged.| + +You can also group resizers through `MultiResizer`, which applies resizers in chain: + +```kotlin +// First scales down, then ensures size is at most 1000. Order matters! +val resizer: Resizer = MultiResizer() +resizer.addResizer(FractionResizer(0.5F)) +resizer.addResizer(AtMostResizer(1000)) + +// First makes it 16:9, then ensures size is at most 1000. Order matters! +val resizer: Resizer = MultiResizer() +resizer.addResizer(AspectRatioResizer(16F / 9F)) +resizer.addResizer(AtMostResizer(1000)) +``` + +This option is already available through the DefaultVideoStrategy builder, so you can do: + +```kotlin +val strategy: DefaultVideoStrategy = DefaultVideoStrategy.Builder() + .addResizer(AspectRatioResizer(16F / 9F)) + .addResizer(FractionResizer(0.5F)) + .addResizer(AtMostResizer(1000)) + .build() +``` + +### Other options + +You can configure the `DefaultVideoStrategy` with other options unrelated to the video size: + +```kotlin +val strategy: DefaultVideoStrategy = DefaultVideoStrategy.Builder() + .bitRate(bitRate) + .bitRate(DefaultVideoStrategy.BITRATE_UNKNOWN) // tries to estimate + .frameRate(frameRate) // will be capped to the input frameRate + .keyFrameInterval(interval) // interval between key-frames in seconds + .build() +``` + +### Compatibility + +As stated pretty much everywhere, **not all codecs/devices/manufacturers support all sizes/options**. +This is a complex issue which is especially important for video strategies, as a wrong size can lead +to a transcoding error or corrupted file. + +Android platform specifies requirements for manufacturers through the [Compatibility test suite](https://source.android.com/compatibility/cts) (CTS). +Only a few codecs and sizes are **strictly** required to work. + +We collect common presets in the `DefaultVideoStrategies` class: + +```kotlin +Transcoder.into(filePath) + .setVideoTrackStrategy(DefaultVideoStrategies.for720x1280()) // 16:9 + .setVideoTrackStrategy(DefaultVideoStrategies.for360x480()) // 4:3 + // ... +``` \ No newline at end of file diff --git a/docs/validators.mdx b/docs/validators.mdx new file mode 100644 index 00000000..0ee78c4d --- /dev/null +++ b/docs/validators.mdx @@ -0,0 +1,50 @@ +--- +title: Validators +--- + +# Validators + +Validators tell the engine whether the transcoding process should start or not based on the status +of the audio and video track. + +```kotlin +Transcoder.into(filePath) + .setValidator(validator) + // ... +``` + +This can be used, for example, to: + +- avoid transcoding when video resolution is already OK with our needs +- avoid operating on files without an audio/video stream +- avoid operating on files with an audio/video stream + +Validators should implement the `validate(TrackStatus, TrackStatus)` and inspect the status for video +and audio tracks. When `false` is returned, transcoding will complete with the `SUCCESS_NOT_NEEDED` status code. +The TrackStatus enum contains the following values: + +|Value|Meaning| +|-----|-------| +|`TrackStatus.ABSENT`|This track was absent in the source file.| +|`TrackStatus.PASS_THROUGH`|This track is about to be copied as-is in the target file.| +|`TrackStatus.COMPRESSING`|This track is about to be processed and compressed in the target file.| +|`TrackStatus.REMOVING`|This track will be removed in the target file.| + +The `TrackStatus` value depends on the [track strategy](track-strategies) that was used. +We provide a few validators that can be injected for typical usage. + +### DefaultValidator + +This is the default validator and it returns true when any of the track is `COMPRESSING` or `REMOVING`. +In the other cases, transcoding is typically not needed so we abort the operation. + +### WriteAlwaysValidator + +This validator always returns true and as such will always write to target file, no matter the track status, +presence of tracks and so on. For instance, the output container file might have no tracks. + +### WriteVideoValidator + +A Validator that gives priority to the video track. Transcoding will not happen if the video track does not need it, +even if the audio track might need it. If reducing file size is your only concern, this can avoid compressing +files that would not benefit so much from compressing the audio track only. \ No newline at end of file diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index a7de0e56..1953ea43 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -58,7 +58,7 @@ deployer { artifactId = "transcoder" release.version = "0.10.5" release.tag = "v0.10.5" - description = "Accelerated video transcoding using Android MediaCodec API without native code (no LGPL/patent issues)." + description = "Accelerated video compression and transcoding on Android using MediaCodec APIs (no FFMPEG/LGPL licensing issues). Supports cropping to any dimension, concatenation, audio processing and much more." url = "https://github.com/deepmedia/Transcoder" scm.fromGithub("deepmedia", "Transcoder") license(apache2) From fe1bda940d4e2f395969e11f419cb24923b7961e Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Mon, 5 Aug 2024 09:30:27 +0200 Subject: [PATCH 10/14] Fix emulator runtime --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf6c1f60..42cefa2a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: run: ./gradlew demo:assembleDebug lib:deployLocal ANDROID_EMULATOR_TESTS: name: Emulator Tests - runs-on: macos-latest + runs-on: ubuntu-latest strategy: fail-fast: false matrix: From 7232cfb36a1ee0a2f299fc5b035ce1fa996c2f69 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Mon, 5 Aug 2024 09:33:59 +0200 Subject: [PATCH 11/14] Fix deployer dependency --- lib/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 1953ea43..97a98c0f 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -1,7 +1,7 @@ plugins { id("com.android.library") kotlin("android") - id("io.deepmedia.tools.deployer") version "0.14.0-local-alpha1" + id("io.deepmedia.tools.deployer") version "0.14.0-alpha1" id("org.jetbrains.dokka") version "1.9.20" } From af1541cd3b8e55634dbff6c304c76216a2a7a563 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Mon, 5 Aug 2024 09:58:45 +0200 Subject: [PATCH 12/14] Min SDK version bump to 21 --- README.md | 2 +- demo/build.gradle.kts | 2 +- docs/index.mdx | 2 +- lib/build.gradle.kts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9bd98b1e..a44321ad 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ # Transcoder Transcodes and compresses video files into the MP4 format, with audio support, using hardware-accelerated -Android codecs available on the device. Works on API 18+. +Android codecs available on the device. Works on API 21+. - Fast transcoding to AAC/AVC - Hardware accelerated diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 75602280..93fcf8ac 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -7,7 +7,7 @@ android { namespace = "com.otaliastudios.transcoder.demo" compileSdk = 34 defaultConfig { - minSdk = 18 + minSdk = 21 targetSdk = 34 versionCode = 1 versionName = "1.0" diff --git a/docs/index.mdx b/docs/index.mdx index baa72588..fe762288 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -15,7 +15,7 @@ docs: # Intro The Transcoder library transcodes and compresses video files into the MP4 format, with audio support, using hardware-accelerated -Android codecs available on the device. Works on API 18+ and supports the following set of features: +Android codecs available on the device. Works on API 19+ and supports the following set of features: - Fast transcoding to AAC/AVC - Hardware accelerated diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 97a98c0f..533e7902 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -9,7 +9,7 @@ android { namespace = "com.otaliastudios.transcoder" compileSdk = 34 defaultConfig { - minSdk = 18 + minSdk = 21 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } testOptions { From 0c77eba7e84846c230ea2ce47541e11e136d499c Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Mon, 5 Aug 2024 10:07:53 +0200 Subject: [PATCH 13/14] Enable emulator stacktrace --- .github/workflows/emulator_script.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/emulator_script.sh b/.github/workflows/emulator_script.sh index 921467b1..af4ea4bb 100755 --- a/.github/workflows/emulator_script.sh +++ b/.github/workflows/emulator_script.sh @@ -5,4 +5,4 @@ ADB_TAGS="$ADB_TAGS VideoDecoderOutput:I VideoFrameDropper:I" ADB_TAGS="$ADB_TAGS AudioEngine:I" adb logcat -c adb logcat $ADB_TAGS *:E -v color & -./gradlew lib:connectedCheck \ No newline at end of file +./gradlew lib:connectedCheck --stacktrace \ No newline at end of file From c3f91623f8910a98094c2a4866baaa07dcd75282 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Mon, 5 Aug 2024 10:27:49 +0200 Subject: [PATCH 14/14] Read signing info --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42cefa2a..e7a951e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,10 @@ jobs: ANDROID_EMULATOR_TESTS: name: Emulator Tests runs-on: ubuntu-latest + # Temporary workaround for deployer issue + env: + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} strategy: fail-fast: false matrix: