mirror of
https://github.com/samsonjs/media.git
synced 2026-04-18 13:25:47 +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.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. */
|
||||
|
|
|
|||
|
|
@ -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