Commit abccbcf2 by kimvde Committed by kim-vde

Publish transformer module

PiperOrigin-RevId: 353254249
parent 5b9fa7d7
Showing with 878 additions and 0 deletions
......@@ -139,6 +139,10 @@
* Remove `setVideoDecoderOutputBufferRenderer` from Player API. Use
`setVideoSurfaceView` and `clearVideoSurfaceView` instead.
* Replace `PlayerMessage.setHandler` with `PlayerMessage.setLooper`.
* Transformer:
* Add a library to transform media inputs. Available transformations are:
configuration of output container format, removal of audio or video
track and slow motion flattening.
* Extractors:
* Populate codecs string for H.264/AVC in MP4, Matroska and FLV streams to
allow decoder capability checks based on codec profile/level
......
......@@ -28,6 +28,7 @@ include modulePrefix + 'library-dash'
include modulePrefix + 'library-extractor'
include modulePrefix + 'library-hls'
include modulePrefix + 'library-smoothstreaming'
include modulePrefix + 'library-transformer'
include modulePrefix + 'library-ui'
include modulePrefix + 'robolectricutils'
include modulePrefix + 'testutils'
......@@ -56,6 +57,7 @@ project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/d
project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor')
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
project(modulePrefix + 'library-transformer').projectDir = new File(rootDir, 'library/transformer')
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
project(modulePrefix + 'robolectricutils').projectDir = new File(rootDir, 'robolectricutils')
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
......
# ExoPlayer transformer library module #
Provides support for transforming media files.
## Links ##
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.transformer.*`
belong to this module.
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
// Copyright 2020 The Android Open Source Project
//
// 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.
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
android {
buildTypes {
debug {
testCoverageEnabled = true
}
}
sourceSets.test.assets.srcDir '../../testdata/src/test/assets/'
}
dependencies {
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'library-core')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
testImplementation project(modulePrefix + 'robolectricutils')
testImplementation project(modulePrefix + 'testutils')
testImplementation project(modulePrefix + 'testdata')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
javadocTitle = 'Transformer module'
}
apply from: '../../javadoc_library.gradle'
ext {
releaseArtifact = 'exoplayer-transformer'
releaseDescription = 'The ExoPlayer library transformer module.'
}
apply from: '../../publish.gradle'
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 The Android Open Source Project
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.
-->
<manifest package="com.google.android.exoplayer2.transformer"/>
/*
* Copyright 2020 The Android Open Source Project
*
* 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.
*/
package com.google.android.exoplayer2.transformer;
import androidx.annotation.IntRange;
/** Holds a progress percentage. */
public final class ProgressHolder {
/** The held progress, expressed as an integer percentage. */
@IntRange(from = 0, to = 100)
public int progress;
}
/*
* Copyright 2020 The Android Open Source Project
*
* 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.
*/
package com.google.android.exoplayer2.transformer;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
/** A sample transformer for a given track. */
/* package */ interface SampleTransformer {
/**
* Transforms the data and metadata of the sample contained in {@code buffer}.
*
* @param buffer The sample to transform. If the sample {@link DecoderInputBuffer#data data} is
* {@code null} after the execution of this method, the sample must be discarded.
*/
void transformSample(DecoderInputBuffer buffer);
}
/*
* Copyright 2021 The Android Open Source Project
*
* 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.
*/
package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.metadata.mp4.SlowMotionData.Segment.BY_START_THEN_END_THEN_DIVISOR;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.mp4.SlowMotionData;
import com.google.android.exoplayer2.metadata.mp4.SlowMotionData.Segment;
import com.google.android.exoplayer2.metadata.mp4.SmtaMetadataEntry;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/** A {@link SpeedProvider} for slow motion segments. */
/* package */ class SegmentSpeedProvider implements SpeedProvider {
/**
* Input frame rate of Samsung Slow motion videos is always 30. See
* go/exoplayer-sef-slomo-video-flattening.
*/
private static final int INPUT_FRAME_RATE = 30;
private final ImmutableSortedMap<Long, Float> speedsByStartTimeUs;
private final float baseSpeedMultiplier;
public SegmentSpeedProvider(Format format) {
float captureFrameRate = getCaptureFrameRate(format);
this.baseSpeedMultiplier =
captureFrameRate == C.RATE_UNSET ? 1 : captureFrameRate / INPUT_FRAME_RATE;
this.speedsByStartTimeUs = buildSpeedByStartTimeUsMap(format, baseSpeedMultiplier);
}
@Override
public float getSpeed(long timeUs) {
checkArgument(timeUs >= 0);
@Nullable Map.Entry<Long, Float> entry = speedsByStartTimeUs.floorEntry(timeUs);
return entry != null ? entry.getValue() : baseSpeedMultiplier;
}
private static ImmutableSortedMap<Long, Float> buildSpeedByStartTimeUsMap(
Format format, float baseSpeed) {
List<Segment> segments = extractSlowMotionSegments(format);
if (segments.isEmpty()) {
return ImmutableSortedMap.of();
}
TreeMap<Long, Float> speedsByStartTimeUs = new TreeMap<>();
// Start time maps to the segment speed.
for (int i = 0; i < segments.size(); i++) {
Segment currentSegment = segments.get(i);
speedsByStartTimeUs.put(
C.msToUs(currentSegment.startTimeMs), baseSpeed / currentSegment.speedDivisor);
}
// If the map has an entry at endTime, this is the next segments start time. If no such entry
// exists, map the endTime to base speed because the times after the end time are not in a
// segment.
for (int i = 0; i < segments.size(); i++) {
Segment currentSegment = segments.get(i);
if (!speedsByStartTimeUs.containsKey(C.msToUs(currentSegment.endTimeMs))) {
speedsByStartTimeUs.put(C.msToUs(currentSegment.endTimeMs), baseSpeed);
}
}
return ImmutableSortedMap.copyOf(speedsByStartTimeUs);
}
private static float getCaptureFrameRate(Format format) {
@Nullable Metadata metadata = format.metadata;
if (metadata == null) {
return C.RATE_UNSET;
}
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof SmtaMetadataEntry) {
return ((SmtaMetadataEntry) entry).captureFrameRate;
}
}
return C.RATE_UNSET;
}
private static ImmutableList<Segment> extractSlowMotionSegments(Format format) {
List<Segment> segments = new ArrayList<>();
@Nullable Metadata metadata = format.metadata;
if (metadata != null) {
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof SlowMotionData) {
segments.addAll(((SlowMotionData) entry).segments);
}
}
}
return ImmutableList.sortedCopyOf(BY_START_THEN_END_THEN_DIVISOR, segments);
}
}
/*
* Copyright 2021 The Android Open Source Project
*
* 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.
*/
package com.google.android.exoplayer2.transformer;
/** A custom interface that determines the speed for media at specific timestamps. */
public interface SpeedProvider {
/**
* Provides the speed that the media should be played at, based on the timeUs.
*
* @param timeUs The timestamp of the media.
* @return The speed that the media should be played at, based on the timeUs.
*/
float getSpeed(long timeUs);
}
/*
* Copyright 2020 The Android Open Source Project
*
* 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.
*/
package com.google.android.exoplayer2.transformer;
/** A media transformation configuration. */
/* package */ final class Transformation {
public final boolean removeAudio;
public final boolean removeVideo;
public final boolean flattenForSlowMotion;
public final String outputMimeType;
public Transformation(
boolean removeAudio,
boolean removeVideo,
boolean flattenForSlowMotion,
String outputMimeType) {
this.removeAudio = removeAudio;
this.removeVideo = removeVideo;
this.flattenForSlowMotion = flattenForSlowMotion;
this.outputMimeType = outputMimeType;
}
}
/*
* Copyright 2020 The Android Open Source Project
*
* 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.
*/
package com.google.android.exoplayer2.transformer;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.MimeTypes;
@RequiresApi(18)
/* package */ abstract class TransformerBaseRenderer extends BaseRenderer {
protected final MuxerWrapper muxerWrapper;
protected final TransformerMediaClock mediaClock;
protected final Transformation transformation;
protected boolean isRendererStarted;
public TransformerBaseRenderer(
int trackType,
MuxerWrapper muxerWrapper,
TransformerMediaClock mediaClock,
Transformation transformation) {
super(trackType);
this.muxerWrapper = muxerWrapper;
this.mediaClock = mediaClock;
this.transformation = transformation;
}
@Override
@C.FormatSupport
public final int supportsFormat(Format format) {
@Nullable String sampleMimeType = format.sampleMimeType;
if (MimeTypes.getTrackType(format.sampleMimeType) != getTrackType()) {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
} else if (muxerWrapper.supportsSampleMimeType(sampleMimeType)) {
return RendererCapabilities.create(C.FORMAT_HANDLED);
} else {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE);
}
}
@Override
public final boolean isReady() {
return isSourceReady();
}
@Override
public final MediaClock getMediaClock() {
return mediaClock;
}
@Override
protected final void onEnabled(boolean joining, boolean mayRenderStartOfStream) {
muxerWrapper.registerTrack();
mediaClock.updateTimeForTrackType(getTrackType(), 0L);
}
@Override
protected final void onStarted() {
isRendererStarted = true;
}
@Override
protected final void onStopped() {
isRendererStarted = false;
}
}
/*
* Copyright 2020 The Android Open Source Project
*
* 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.
*/
package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.util.Util.minValue;
import android.util.SparseLongArray;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.util.MediaClock;
@RequiresApi(18)
/* package */ final class TransformerMediaClock implements MediaClock {
private final SparseLongArray trackTypeToTimeUs;
private long minTrackTimeUs;
public TransformerMediaClock() {
trackTypeToTimeUs = new SparseLongArray();
}
/**
* Updates the time for a given track type. The clock time is computed based on the different
* track times.
*/
public void updateTimeForTrackType(int trackType, long timeUs) {
long previousTimeUs = trackTypeToTimeUs.get(trackType, /* valueIfKeyNotFound= */ C.TIME_UNSET);
if (previousTimeUs != C.TIME_UNSET && timeUs <= previousTimeUs) {
// Make sure that the track times are increasing and therefore that the clock time is
// increasing. This is necessary for progress updates.
return;
}
trackTypeToTimeUs.put(trackType, timeUs);
if (previousTimeUs == C.TIME_UNSET || previousTimeUs == minTrackTimeUs) {
minTrackTimeUs = minValue(trackTypeToTimeUs);
}
}
@Override
public long getPositionUs() {
// Use minimum position among tracks as position to ensure that the buffered duration is
// positive. This is also useful for controlling samples interleaving.
return minTrackTimeUs;
}
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {}
@Override
public PlaybackParameters getPlaybackParameters() {
// Playback parameters are unknown. Set default value.
return PlaybackParameters.DEFAULT;
}
}
/*
* Copyright 2020 The Android Open Source Project
*
* 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.
*/
package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.SampleStream;
import java.nio.ByteBuffer;
@RequiresApi(18)
/* package */ final class TransformerVideoRenderer extends TransformerBaseRenderer {
private static final String TAG = "TransformerVideoRenderer";
private final DecoderInputBuffer buffer;
@Nullable private SampleTransformer sampleTransformer;
private boolean formatRead;
private boolean isBufferPending;
private boolean isInputStreamEnded;
public TransformerVideoRenderer(
MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, Transformation transformation) {
super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformation);
buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
}
@Override
public String getName() {
return TAG;
}
@Override
public void render(long positionUs, long elapsedRealtimeUs) {
if (!isRendererStarted || isEnded()) {
return;
}
if (!formatRead) {
FormatHolder formatHolder = getFormatHolder();
@SampleStream.ReadDataResult
int result = readSource(formatHolder, buffer, /* formatRequired= */ true);
if (result != C.RESULT_FORMAT_READ) {
return;
}
Format format = checkNotNull(formatHolder.format);
formatRead = true;
if (transformation.flattenForSlowMotion) {
sampleTransformer = new SefSlowMotionVideoSampleTransformer(format);
}
muxerWrapper.addTrackFormat(format);
}
while (true) {
// Read sample.
if (!isBufferPending && !readAndTransformBuffer()) {
return;
}
// Write sample.
isBufferPending =
!muxerWrapper.writeSample(
getTrackType(), buffer.data, buffer.isKeyFrame(), buffer.timeUs);
if (isBufferPending) {
return;
}
}
}
@Override
public boolean isEnded() {
return isInputStreamEnded;
}
/**
* Checks whether a sample can be read and, if so, reads it, transforms it and writes the
* resulting sample to the {@link #buffer}.
*
* <p>The buffer data can be set to null if the transformation applied discards the sample.
*
* @return Whether a sample has been read and transformed.
*/
private boolean readAndTransformBuffer() {
buffer.clear();
@SampleStream.ReadDataResult
int result = readSource(getFormatHolder(), buffer, /* formatRequired= */ false);
if (result == C.RESULT_FORMAT_READ) {
throw new IllegalStateException("Format changes are not supported.");
} else if (result == C.RESULT_NOTHING_READ) {
return false;
}
// Buffer read.
if (buffer.isEndOfStream()) {
isInputStreamEnded = true;
muxerWrapper.endTrack(getTrackType());
return false;
}
mediaClock.updateTimeForTrackType(getTrackType(), buffer.timeUs);
ByteBuffer data = checkNotNull(buffer.data);
data.flip();
if (sampleTransformer != null) {
sampleTransformer.transformSample(buffer);
}
return true;
}
}
/*
* Copyright 2020 The Android Open Source Project
*
* 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.
*/
@NonNullApi
package com.google.android.exoplayer2.transformer;
import com.google.android.exoplayer2.util.NonNullApi;
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 The Android Open Source Project
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.
-->
<manifest package="com.google.android.exoplayer2.transformer.test">
<uses-sdk/>
</manifest>
/*
* Copyright 2021 The Android Open Source Project
*
* 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.
*/
package com.google.android.exoplayer2.transformer;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.mp4.SlowMotionData;
import com.google.android.exoplayer2.metadata.mp4.SlowMotionData.Segment;
import com.google.android.exoplayer2.metadata.mp4.SmtaMetadataEntry;
import com.google.common.collect.ImmutableList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link SegmentSpeedProvider}. */
@RunWith(AndroidJUnit4.class)
public class SegmentSpeedProviderTest {
private static final SmtaMetadataEntry SMTA_SPEED_8 =
new SmtaMetadataEntry(/* captureFrameRate= */ 240, /* svcTemporalLayerCount= */ 4);
@Test
public void getSpeed_noSegments_returnsBaseSpeed() {
SegmentSpeedProvider provider =
new SegmentSpeedProvider(
new Format.Builder().setMetadata(new Metadata(SMTA_SPEED_8)).build());
assertThat(provider.getSpeed(0)).isEqualTo(8);
assertThat(provider.getSpeed(1_000_000)).isEqualTo(8);
}
@Test
public void getSpeed_returnsCorrectSpeed() {
List<Segment> segments =
ImmutableList.of(
new Segment(/* startTimeMs= */ 500, /* endTimeMs= */ 1000, /* speedDivisor= */ 8),
new Segment(/* startTimeMs= */ 1500, /* endTimeMs= */ 2000, /* speedDivisor= */ 4),
new Segment(/* startTimeMs= */ 2000, /* endTimeMs= */ 2500, /* speedDivisor= */ 2));
SegmentSpeedProvider provider =
new SegmentSpeedProvider(
new Format.Builder()
.setMetadata(new Metadata(new SlowMotionData(segments), SMTA_SPEED_8))
.build());
assertThat(provider.getSpeed(C.msToUs(0))).isEqualTo(8);
assertThat(provider.getSpeed(C.msToUs(500))).isEqualTo(1);
assertThat(provider.getSpeed(C.msToUs(800))).isEqualTo(1);
assertThat(provider.getSpeed(C.msToUs(1000))).isEqualTo(8);
assertThat(provider.getSpeed(C.msToUs(1250))).isEqualTo(8);
assertThat(provider.getSpeed(C.msToUs(1500))).isEqualTo(2);
assertThat(provider.getSpeed(C.msToUs(1650))).isEqualTo(2);
assertThat(provider.getSpeed(C.msToUs(2000))).isEqualTo(4);
assertThat(provider.getSpeed(C.msToUs(2400))).isEqualTo(4);
assertThat(provider.getSpeed(C.msToUs(2500))).isEqualTo(8);
assertThat(provider.getSpeed(C.msToUs(3000))).isEqualTo(8);
}
@Test
public void getSpeed_withNegativeTimestamp_throwsException() {
assertThrows(
IllegalArgumentException.class,
() ->
new SegmentSpeedProvider(
new Format.Builder().setMetadata(new Metadata(SMTA_SPEED_8)).build())
.getSpeed(-1));
}
}
/*
* Copyright 2020 The Android Open Source Project
*
* 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.
*/
package com.google.android.exoplayer2.transformer;
import static org.junit.Assert.assertThrows;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.util.MimeTypes;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link Transformer.Builder}. */
@RunWith(AndroidJUnit4.class)
public class TransformerBuilderTest {
@Test
public void setOutputMimeType_unsupportedMimeType_throws() {
assertThrows(
IllegalArgumentException.class,
() -> new Transformer.Builder().setOutputMimeType(MimeTypes.VIDEO_FLV));
}
@Test
public void build_withoutContext_throws() {
assertThrows(IllegalStateException.class, () -> new Transformer.Builder().build());
}
@Test
public void build_removeAudioAndVideo_throws() {
Context context = ApplicationProvider.getApplicationContext();
assertThrows(
IllegalStateException.class,
() ->
new Transformer.Builder()
.setContext(context)
.setRemoveAudio(true)
.setRemoveVideo(true)
.build());
}
}
/*
* Copyright 2020 The Android Open Source Project
*
* 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.
*/
package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runLooperUntil;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.robolectric.RobolectricUtil;
import java.util.concurrent.TimeoutException;
/** Helper class to run a {@link Transformer} test. */
public final class TransformerTestRunner {
private TransformerTestRunner() {}
/**
* Runs tasks of the {@link Transformer#getApplicationLooper() transformer Looper} until the
* current {@link Transformer transformation} completes.
*
* @param transformer The {@link Transformer}.
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
* exceeded.
* @throws IllegalStateException If the method is not called from the main thread, or if the
* transformation completes with error.
*/
public static void runUntilCompleted(Transformer transformer) throws TimeoutException {
@Nullable Exception exception = runUntilListenerCalled(transformer);
if (exception != null) {
throw new IllegalStateException(exception);
}
}
/**
* Runs tasks of the {@link Transformer#getApplicationLooper() transformer Looper} until a {@link
* Transformer} error occurs.
*
* @param transformer The {@link Transformer}.
* @return The raised exception.
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
* exceeded.
* @throws IllegalStateException If the method is not called from the main thread, or if the
* transformation completes without error.
*/
public static Exception runUntilError(Transformer transformer) throws TimeoutException {
@Nullable Exception exception = runUntilListenerCalled(transformer);
if (exception == null) {
throw new IllegalStateException("The transformation completed without error.");
}
return exception;
}
@Nullable
private static Exception runUntilListenerCalled(Transformer transformer) throws TimeoutException {
TransformationResult transformationResult = new TransformationResult();
Transformer.Listener listener =
new Transformer.Listener() {
@Override
public void onTransformationCompleted(MediaItem inputMediaItem) {
transformationResult.isCompleted = true;
}
@Override
public void onTransformationError(MediaItem inputMediaItem, Exception exception) {
transformationResult.exception = exception;
}
};
transformer.setListener(listener);
runLooperUntil(
transformer.getApplicationLooper(),
() -> transformationResult.isCompleted || transformationResult.exception != null);
return transformationResult.exception;
}
private static class TransformationResult {
public boolean isCompleted;
@Nullable public Exception exception;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment