Skip to content

Speed support #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jul 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 104 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,27 @@ Transcoder.into(filePath)

Take a look at the demo app for a real example or keep reading below for documentation.

*Note: this project is an improved fork of [ypresto/android-transcoder](https://github.com/ypresto/android-transcoder).
It features a lot of improvements over the original project, including:*

- *Multithreading support*
- *Crop to any aspect ratio*
- *Set output video rotation*
- *Various bugs fixed*
- *[Input](#data-sources): Accept content Uris and other types*
- *[Real error handling](#listening-for-events) instead of errors being thrown*
- *Frame dropping support, which means you can set the video frame rate*
- *Source project is over-conservative when choosing options that *might* not be supported. We prefer to try and let the codec fail*
- *More convenient APIs for transcoding & choosing options*
- *Configurable [Validators](#validators) to e.g. **not** perform transcoding if the source video is already compressed enough*
- *Expose internal logs through Logger (so they can be reported to e.g. Crashlytics)*
- *Handy utilities for track configuration through [Output Strategies](#output-strategies)*
- *Handy utilities for resizing*
## Features

- Fast transcoding to AAC/AVC
- Hardware accelerated
- Multithreaded
- Convenient, fluent API
- Choose output size, with automatic cropping [[docs]](#video-size)
- Choose output rotation [[docs]](#video-rotation)
- Choose output speed [[docs]](#video-speed)
- Choose output frame rate [[docs]](#other-options)
- Choose output audio channels [[docs]](#audio-strategies)
- Override frames timestamp, e.g. to slow down the middle part of the video [[docs]](#time-interpolation)
- Error handling [[docs]](#listening-for-events)
- Configurable validators to e.g. avoid transcoding if the source is already compressed enough [[docs]](#validators)
- Configurable video and audio strategies [[docs]](#output-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*.

## Setup

Expand Down Expand Up @@ -128,8 +133,8 @@ Transcoding operation did succeed. The success code can be:

|Code|Meaning|
|----|-------|
|`MediaTranscoder.SUCCESS_TRANSCODED`|Transcoding was executed successfully. Transcoded file was written to the output path.|
|`MediaTranscoder.SUCCESS_NOT_NEEDED`|Transcoding was not executed because it was considered **not needed** by the `Validator`.|
|`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 [below](#validators) to know about `Validator`s.

Expand Down Expand Up @@ -220,9 +225,9 @@ audio stream to AAC format with the specified number of channels.

```java
Transcoder.into(filePath)
.setAudioOutputStrategy(DefaultAudioStrategy(1)) // or..
.setAudioOutputStrategy(DefaultAudioStrategy(2)) // or..
.setAudioOutputStrategy(DefaultAudioStrategy(DefaultAudioStrategy.AUDIO_CHANNELS_AS_IS))
.setAudioOutputStrategy(new DefaultAudioStrategy(1)) // or..
.setAudioOutputStrategy(new DefaultAudioStrategy(2)) // or..
.setAudioOutputStrategy(new DefaultAudioStrategy(DefaultAudioStrategy.AUDIO_CHANNELS_AS_IS))
// ...
```

Expand All @@ -243,16 +248,16 @@ We provide helpers for common tasks:
DefaultVideoStrategy strategy;

// Sets an exact size. If aspect ratio does not match, cropping will take place.
strategy = DefaultVideoStrategy.exact(1080, 720).build()
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()
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()
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()
strategy = DefaultVideoStrategy.atMost(500, 1000).build();
```

In fact, all of these will simply call `new DefaultVideoStrategy.Builder(resizer)` with a special
Expand All @@ -270,14 +275,14 @@ You can also group resizers through `MultiResizer`, which applies resizers in ch

```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))
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))
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:
Expand All @@ -287,7 +292,7 @@ DefaultVideoStrategy strategy = new DefaultVideoStrategy.Builder()
.addResizer(new AspectRatioResizer(16F / 9F))
.addResizer(new FractionResizer(0.5F))
.addResizer(new AtMostResizer(1000))
.build()
.build();
```

### Other options
Expand All @@ -300,10 +305,10 @@ DefaultVideoStrategy strategy = new DefaultVideoStrategy.Builder()
.bitRate(DefaultVideoStrategy.BITRATE_UNKNOWN) // tries to estimate
.frameRate(frameRate) // will be capped to the input frameRate
.iFrameInterval(interval) // interval between I-frames in seconds
.build()
.build();
```

## Other Options
## Advanced Options

#### Video rotation

Expand All @@ -316,14 +321,78 @@ Transcoder.into(filePath)
// ...
```

#### 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.

## 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.
Only a few codecs and sizes are **strictly** required to work.

We collect common presets in the `DefaultVideoStrategies` class:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@

import com.otaliastudios.transcoder.Transcoder;
import com.otaliastudios.transcoder.TranscoderListener;
import com.otaliastudios.transcoder.engine.TrackStatus;
import com.otaliastudios.transcoder.internal.Logger;
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy;
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy;
import com.otaliastudios.transcoder.strategy.OutputStrategy;
import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy;
import com.otaliastudios.transcoder.strategy.size.AspectRatioResizer;
import com.otaliastudios.transcoder.strategy.size.FractionResizer;
import com.otaliastudios.transcoder.strategy.size.PassThroughResizer;
import com.otaliastudios.transcoder.validator.DefaultValidator;

import java.io.File;
import java.io.IOException;
Expand All @@ -44,6 +47,8 @@ public class TranscoderActivity extends AppCompatActivity implements
private RadioGroup mVideoResolutionGroup;
private RadioGroup mVideoAspectGroup;
private RadioGroup mVideoRotationGroup;
private RadioGroup mSpeedGroup;

private ProgressBar mProgressView;
private TextView mButtonView;

Expand Down Expand Up @@ -79,6 +84,8 @@ protected void onCreate(Bundle savedInstanceState) {
mVideoResolutionGroup = findViewById(R.id.resolution);
mVideoAspectGroup = findViewById(R.id.aspect);
mVideoRotationGroup = findViewById(R.id.rotation);
mSpeedGroup = findViewById(R.id.speed);

mAudioChannelsGroup.setOnCheckedChangeListener(this);
mVideoFramesGroup.setOnCheckedChangeListener(this);
mVideoResolutionGroup.setOnCheckedChangeListener(this);
Expand Down Expand Up @@ -166,6 +173,13 @@ private void transcode() {
default: rotation = 0;
}

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;
}

// Launch the transcoding operation.
mTranscodeStartTime = SystemClock.uptimeMillis();
setIsTranscoding(true);
Expand All @@ -175,6 +189,7 @@ private void transcode() {
.setAudioOutputStrategy(mTranscodeAudioStrategy)
.setVideoOutputStrategy(mTranscodeVideoStrategy)
.setRotation(rotation)
.setSpeed(speed)
.transcode();
}

Expand Down
37 changes: 37 additions & 0 deletions demo/src/main/res/layout/activity_transcoder.xml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,43 @@
android:layout_height="wrap_content" />
</RadioGroup>


<!-- VIDEO ROTATION -->
<TextView
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Playback speed" />
<RadioGroup
android:id="@+id/speed"
android:checkedButton="@id/speed_1x"
android:orientation="horizontal"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/speed_05x"
android:text="0.5x"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/speed_1x"
android:text="1x"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/speed_2x"
android:text="2x"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RadioGroup>

<!-- INFO TEXT -->
<TextView
android:padding="16dp"
Expand Down
Loading