Commit f747fed8 by hschlueter Committed by tonihei

Add FallbackListener.

The app will be notified about fallback using a callback on
Transformer.Listener. Fallback may be applied separately for
the audio and video options, so an intermediate internal
FallbackListener is needed to accumulate and merge the track-specific
changes to the TransformationRequest.

PiperOrigin-RevId: 421839991
parent 308eaf55
/*
* Copyright 2022 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.checkState;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.util.ListenerSet;
import com.google.android.exoplayer2.util.Util;
/**
* Listener for fallback {@link TransformationRequest TransformationRequests} from the audio and
* video renderers.
*/
/* package */ final class FallbackListener {
private final MediaItem mediaItem;
private final TransformationRequest originalTransformationRequest;
private final ListenerSet<Transformer.Listener> transformerListeners;
private TransformationRequest fallbackTransformationRequest;
private int trackCount;
/**
* Creates a new instance.
*
* @param mediaItem The {@link MediaItem} to transform.
* @param transformerListeners The {@link Transformer.Listener listeners} to forward events to.
* @param originalTransformationRequest The original {@link TransformationRequest}.
*/
public FallbackListener(
MediaItem mediaItem,
ListenerSet<Transformer.Listener> transformerListeners,
TransformationRequest originalTransformationRequest) {
this.mediaItem = mediaItem;
this.transformerListeners = transformerListeners;
this.originalTransformationRequest = originalTransformationRequest;
this.fallbackTransformationRequest = originalTransformationRequest;
}
/**
* Registers an output track.
*
* <p>All tracks must be registered before a transformation request is {@link
* #onTransformationRequestFinalized(TransformationRequest) finalized}.
*/
public void registerTrack() {
trackCount++;
}
/**
* Updates the fallback {@link TransformationRequest}.
*
* <p>Should be called with the final {@link TransformationRequest} for each track after all
* fallback has been applied. Calls {@link Transformer.Listener#onFallbackApplied(MediaItem,
* TransformationRequest, TransformationRequest)} once this method has been called for each track.
*
* @param transformationRequest The final {@link TransformationRequest} for a track.
* @throws IllegalStateException If called for more tracks than registered using {@link
* #registerTrack()}.
*/
public void onTransformationRequestFinalized(TransformationRequest transformationRequest) {
checkState(trackCount-- > 0);
TransformationRequest.Builder fallbackRequestBuilder =
fallbackTransformationRequest.buildUpon();
if (!Util.areEqual(
transformationRequest.audioMimeType, originalTransformationRequest.audioMimeType)) {
fallbackRequestBuilder.setAudioMimeType(transformationRequest.audioMimeType);
}
if (!Util.areEqual(
transformationRequest.videoMimeType, originalTransformationRequest.videoMimeType)) {
fallbackRequestBuilder.setVideoMimeType(transformationRequest.videoMimeType);
}
if (transformationRequest.outputHeight != originalTransformationRequest.outputHeight) {
fallbackRequestBuilder.setResolution(transformationRequest.outputHeight);
}
fallbackTransformationRequest = fallbackRequestBuilder.build();
if (trackCount == 0 && !originalTransformationRequest.equals(fallbackTransformationRequest)) {
transformerListeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener ->
listener.onFallbackApplied(
mediaItem, originalTransformationRequest, fallbackTransformationRequest));
transformerListeners.flushEvents();
}
}
}
......@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableSet;
/** A media transformation request. */
public final class TransformationRequest {
......@@ -30,6 +31,9 @@ public final class TransformationRequest {
/** A builder for {@link TransformationRequest} instances. */
public static final class Builder {
private static final ImmutableSet<Integer> SUPPORTED_OUTPUT_HEIGHTS =
ImmutableSet.of(144, 240, 360, 480, 720, 1080, 1440, 2160);
private Matrix transformationMatrix;
private boolean flattenForSlowMotion;
private int outputHeight;
......@@ -113,8 +117,9 @@ public final class TransformationRequest {
}
/**
* Sets the output resolution using the output height. The default value is the same height as
* the input. Output width will scale to preserve the input video's aspect ratio.
* Sets the output resolution using the output height. The default value {@link C#LENGTH_UNSET}
* corresponds to using the same height as the input. Output width will scale to preserve the
* input video's aspect ratio.
*
* <p>For now, only "popular" heights like 144, 240, 360, 480, 720, 1080, 1440, or 2160 are
* supported, to ensure compatibility on different devices.
......@@ -128,24 +133,16 @@ public final class TransformationRequest {
// TODO(b/201293185): Restructure to input a Presentation class.
// TODO(b/201293185): Check encoder codec capabilities in order to allow arbitrary
// resolutions and reasonable fallbacks.
if (outputHeight != 144
&& outputHeight != 240
&& outputHeight != 360
&& outputHeight != 480
&& outputHeight != 720
&& outputHeight != 1080
&& outputHeight != 1440
&& outputHeight != 2160) {
throw new IllegalArgumentException(
"Please use a height of 144, 240, 360, 480, 720, 1080, 1440, or 2160.");
if (outputHeight != C.LENGTH_UNSET && !SUPPORTED_OUTPUT_HEIGHTS.contains(outputHeight)) {
throw new IllegalArgumentException("Unsupported outputHeight: " + outputHeight);
}
this.outputHeight = outputHeight;
return this;
}
/**
* Sets the video MIME type of the output. The default value is to use the same MIME type as the
* input. Supported values are:
* Sets the video MIME type of the output. The default value is {@code null} which corresponds
* to using the same MIME type as the input. Supported MIME types are:
*
* <ul>
* <li>{@link MimeTypes#VIDEO_H263}
......@@ -157,7 +154,7 @@ public final class TransformationRequest {
* @param videoMimeType The MIME type of the video samples in the output.
* @return This builder.
*/
public Builder setVideoMimeType(String videoMimeType) {
public Builder setVideoMimeType(@Nullable String videoMimeType) {
// TODO(b/209469847): Validate videoMimeType here once deprecated
// Transformer.Builder#setOuputMimeType(String) has been removed.
this.videoMimeType = videoMimeType;
......@@ -165,8 +162,8 @@ public final class TransformationRequest {
}
/**
* Sets the audio MIME type of the output. The default value is to use the same MIME type as the
* input. Supported values are:
* Sets the audio MIME type of the output. The default value is {@code null} which corresponds
* to using the same MIME type as the input. Supported MIME types are:
*
* <ul>
* <li>{@link MimeTypes#AUDIO_AAC}
......@@ -177,7 +174,7 @@ public final class TransformationRequest {
* @param audioMimeType The MIME type of the audio samples in the output.
* @return This builder.
*/
public Builder setAudioMimeType(String audioMimeType) {
public Builder setAudioMimeType(@Nullable String audioMimeType) {
// TODO(b/209469847): Validate audioMimeType here once deprecated
// Transformer.Builder#setOuputMimeType(String) has been removed.
this.audioMimeType = audioMimeType;
......
......@@ -462,6 +462,20 @@ public final class Transformer {
*/
default void onTransformationError(
MediaItem inputMediaItem, TransformationException exception) {}
/**
* Called when fallback to an alternative {@link TransformationRequest} is necessary to comply
* with muxer or device constraints.
*
* @param inputMediaItem The {@link MediaItem} for which the transformation is requested.
* @param originalTransformationRequest The unsupported {@link TransformationRequest} used when
* building {@link Transformer}.
* @param fallbackTransformationRequest The alternative {@link TransformationRequest}.
*/
default void onFallbackApplied(
MediaItem inputMediaItem,
TransformationRequest originalTransformationRequest,
TransformationRequest fallbackTransformationRequest) {}
}
/** Provider for views to show diagnostic information during transformation, for debugging. */
......
/*
* Copyright 2022 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 static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.net.Uri;
import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ListenerSet;
import com.google.android.exoplayer2.util.MimeTypes;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link FallbackListener}. */
@RunWith(AndroidJUnit4.class)
public class FallbackListenerTest {
private static final MediaItem PLACEHOLDER_MEDIA_ITEM = MediaItem.fromUri(Uri.EMPTY);
@Test
public void onTransformationRequestFinalized_withoutTrackRegistration_throwsException() {
TransformationRequest transformationRequest = new TransformationRequest.Builder().build();
FallbackListener fallbackListener =
new FallbackListener(PLACEHOLDER_MEDIA_ITEM, createListenerSet(), transformationRequest);
assertThrows(
IllegalStateException.class,
() -> fallbackListener.onTransformationRequestFinalized(transformationRequest));
}
@Test
public void onTransformationRequestFinalized_afterTrackRegistration_completesSuccessfully() {
TransformationRequest transformationRequest = new TransformationRequest.Builder().build();
FallbackListener fallbackListener =
new FallbackListener(PLACEHOLDER_MEDIA_ITEM, createListenerSet(), transformationRequest);
fallbackListener.registerTrack();
fallbackListener.onTransformationRequestFinalized(transformationRequest);
}
@Test
public void onTransformationRequestFinalized_withUnchangedRequest_doesNotCallback() {
TransformationRequest originalRequest =
new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build();
TransformationRequest unchangedRequest = originalRequest.buildUpon().build();
Transformer.Listener mockListener = mock(Transformer.Listener.class);
FallbackListener fallbackListener =
new FallbackListener(
PLACEHOLDER_MEDIA_ITEM, createListenerSet(mockListener), originalRequest);
fallbackListener.registerTrack();
fallbackListener.onTransformationRequestFinalized(unchangedRequest);
verify(mockListener, never()).onFallbackApplied(any(), any(), any());
}
@Test
public void onTransformationRequestFinalized_withDifferentRequest_callsCallback() {
TransformationRequest originalRequest =
new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build();
TransformationRequest audioFallbackRequest =
new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AMR_WB).build();
Transformer.Listener mockListener = mock(Transformer.Listener.class);
FallbackListener fallbackListener =
new FallbackListener(
PLACEHOLDER_MEDIA_ITEM, createListenerSet(mockListener), originalRequest);
fallbackListener.registerTrack();
fallbackListener.onTransformationRequestFinalized(audioFallbackRequest);
verify(mockListener, times(1))
.onFallbackApplied(PLACEHOLDER_MEDIA_ITEM, originalRequest, audioFallbackRequest);
}
@Test
public void
onTransformationRequestFinalized_forMultipleTracks_callsCallbackOnceWithMergedRequest() {
TransformationRequest originalRequest =
new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build();
TransformationRequest audioFallbackRequest =
originalRequest.buildUpon().setAudioMimeType(MimeTypes.AUDIO_AMR_WB).build();
TransformationRequest videoFallbackRequest =
originalRequest.buildUpon().setVideoMimeType(MimeTypes.VIDEO_H264).build();
TransformationRequest mergedFallbackRequest =
new TransformationRequest.Builder()
.setAudioMimeType(MimeTypes.AUDIO_AMR_WB)
.setVideoMimeType(MimeTypes.VIDEO_H264)
.build();
Transformer.Listener mockListener = mock(Transformer.Listener.class);
FallbackListener fallbackListener =
new FallbackListener(
PLACEHOLDER_MEDIA_ITEM, createListenerSet(mockListener), originalRequest);
fallbackListener.registerTrack();
fallbackListener.registerTrack();
fallbackListener.onTransformationRequestFinalized(audioFallbackRequest);
fallbackListener.onTransformationRequestFinalized(videoFallbackRequest);
verify(mockListener, times(1))
.onFallbackApplied(PLACEHOLDER_MEDIA_ITEM, originalRequest, mergedFallbackRequest);
}
private static ListenerSet<Transformer.Listener> createListenerSet(
Transformer.Listener transformerListener) {
ListenerSet<Transformer.Listener> listenerSet = createListenerSet();
listenerSet.add(transformerListener);
return listenerSet;
}
private static ListenerSet<Transformer.Listener> createListenerSet() {
return new ListenerSet<>(Looper.myLooper(), Clock.DEFAULT, (listener, flags) -> {});
}
}
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