mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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
This commit is contained in:
parent
308eaf55c6
commit
f747fed874
4 changed files with 267 additions and 18 deletions
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* 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.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
|
||||||
/** A media transformation request. */
|
/** A media transformation request. */
|
||||||
public final class TransformationRequest {
|
public final class TransformationRequest {
|
||||||
|
|
@ -30,6 +31,9 @@ public final class TransformationRequest {
|
||||||
/** A builder for {@link TransformationRequest} instances. */
|
/** A builder for {@link TransformationRequest} instances. */
|
||||||
public static final class Builder {
|
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 Matrix transformationMatrix;
|
||||||
private boolean flattenForSlowMotion;
|
private boolean flattenForSlowMotion;
|
||||||
private int outputHeight;
|
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
|
* Sets the output resolution using the output height. The default value {@link C#LENGTH_UNSET}
|
||||||
* the input. Output width will scale to preserve the input video's aspect ratio.
|
* 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
|
* <p>For now, only "popular" heights like 144, 240, 360, 480, 720, 1080, 1440, or 2160 are
|
||||||
* supported, to ensure compatibility on different devices.
|
* 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): Restructure to input a Presentation class.
|
||||||
// TODO(b/201293185): Check encoder codec capabilities in order to allow arbitrary
|
// TODO(b/201293185): Check encoder codec capabilities in order to allow arbitrary
|
||||||
// resolutions and reasonable fallbacks.
|
// resolutions and reasonable fallbacks.
|
||||||
if (outputHeight != 144
|
if (outputHeight != C.LENGTH_UNSET && !SUPPORTED_OUTPUT_HEIGHTS.contains(outputHeight)) {
|
||||||
&& outputHeight != 240
|
throw new IllegalArgumentException("Unsupported outputHeight: " + outputHeight);
|
||||||
&& 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.");
|
|
||||||
}
|
}
|
||||||
this.outputHeight = outputHeight;
|
this.outputHeight = outputHeight;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the video MIME type of the output. The default value is to use the same MIME type as the
|
* Sets the video MIME type of the output. The default value is {@code null} which corresponds
|
||||||
* input. Supported values are:
|
* to using the same MIME type as the input. Supported MIME types are:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@link MimeTypes#VIDEO_H263}
|
* <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.
|
* @param videoMimeType The MIME type of the video samples in the output.
|
||||||
* @return This builder.
|
* @return This builder.
|
||||||
*/
|
*/
|
||||||
public Builder setVideoMimeType(String videoMimeType) {
|
public Builder setVideoMimeType(@Nullable String videoMimeType) {
|
||||||
// TODO(b/209469847): Validate videoMimeType here once deprecated
|
// TODO(b/209469847): Validate videoMimeType here once deprecated
|
||||||
// Transformer.Builder#setOuputMimeType(String) has been removed.
|
// Transformer.Builder#setOuputMimeType(String) has been removed.
|
||||||
this.videoMimeType = videoMimeType;
|
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
|
* Sets the audio MIME type of the output. The default value is {@code null} which corresponds
|
||||||
* input. Supported values are:
|
* to using the same MIME type as the input. Supported MIME types are:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@link MimeTypes#AUDIO_AAC}
|
* <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.
|
* @param audioMimeType The MIME type of the audio samples in the output.
|
||||||
* @return This builder.
|
* @return This builder.
|
||||||
*/
|
*/
|
||||||
public Builder setAudioMimeType(String audioMimeType) {
|
public Builder setAudioMimeType(@Nullable String audioMimeType) {
|
||||||
// TODO(b/209469847): Validate audioMimeType here once deprecated
|
// TODO(b/209469847): Validate audioMimeType here once deprecated
|
||||||
// Transformer.Builder#setOuputMimeType(String) has been removed.
|
// Transformer.Builder#setOuputMimeType(String) has been removed.
|
||||||
this.audioMimeType = audioMimeType;
|
this.audioMimeType = audioMimeType;
|
||||||
|
|
|
||||||
|
|
@ -462,6 +462,20 @@ public final class Transformer {
|
||||||
*/
|
*/
|
||||||
default void onTransformationError(
|
default void onTransformationError(
|
||||||
MediaItem inputMediaItem, TransformationException exception) {}
|
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. */
|
/** Provider for views to show diagnostic information during transformation, for debugging. */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
* 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) -> {});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue