Add previewing specific video graph.

PiperOrigin-RevId: 569473178
This commit is contained in:
claincly 2023-09-29 06:14:11 -07:00 committed by Copybara-Service
parent 0b4638af15
commit 34dddfe9d5
7 changed files with 270 additions and 34 deletions

View file

@ -0,0 +1,56 @@
/*
* Copyright 2023 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
*
* https://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 androidx.media3.common;
import android.content.Context;
import androidx.media3.common.util.UnstableApi;
import java.util.List;
import java.util.concurrent.Executor;
/** A {@link VideoGraph} specific to previewing. */
@UnstableApi
public interface PreviewingVideoGraph extends VideoGraph {
/** A factory for creating a {@link PreviewingVideoGraph}. */
interface Factory {
/**
* Creates a new {@link PreviewingVideoGraph} instance.
*
* @param context A {@link Context}.
* @param inputColorInfo The {@link ColorInfo} for the input frames.
* @param outputColorInfo The {@link ColorInfo} for the output frames.
* @param debugViewProvider A {@link DebugViewProvider}.
* @param listener A {@link Listener}.
* @param listenerExecutor The {@link Executor} on which the {@code listener} is invoked.
* @param compositionEffects A list of {@linkplain Effect effects} to apply to the composition.
* @param initialTimestampOffsetUs The timestamp offset for the first frame, in microseconds.
* @return A new instance.
* @throws VideoFrameProcessingException If a problem occurs while creating the {@link
* VideoFrameProcessor}.
*/
PreviewingVideoGraph create(
Context context,
ColorInfo inputColorInfo,
ColorInfo outputColorInfo,
DebugViewProvider debugViewProvider,
Listener listener,
Executor listenerExecutor,
List<Effect> compositionEffects,
long initialTimestampOffsetUs)
throws VideoFrameProcessingException;
}
}

View file

@ -34,6 +34,14 @@ public interface VideoGraph {
*/ */
void onOutputSizeChanged(int width, int height); void onOutputSizeChanged(int width, int height);
/**
* Called when an output frame with the given {@code presentationTimeUs} becomes available for
* rendering.
*
* @param presentationTimeUs The presentation time of the frame, in microseconds.
*/
void onOutputFrameAvailableForRendering(long presentationTimeUs);
/** /**
* Called after the {@link VideoGraph} has rendered its final output frame. * Called after the {@link VideoGraph} has rendered its final output frame.
* *

View file

@ -0,0 +1,101 @@
/*
* Copyright 2023 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
*
* https://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 androidx.media3.effect;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.media3.common.ColorInfo;
import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.Effect;
import androidx.media3.common.PreviewingVideoGraph;
import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.util.UnstableApi;
import java.util.List;
import java.util.concurrent.Executor;
/**
* A {@link PreviewingVideoGraph Previewing} specific implementation of {@link
* SingleInputVideoGraph}.
*/
@UnstableApi
public final class PreviewingSingleInputVideoGraph extends SingleInputVideoGraph
implements PreviewingVideoGraph {
/** A factory for creating a {@link PreviewingSingleInputVideoGraph}. */
public static final class Factory implements PreviewingVideoGraph.Factory {
private final VideoFrameProcessor.Factory videoFrameProcessorFactory;
public Factory(VideoFrameProcessor.Factory videoFrameProcessorFactory) {
this.videoFrameProcessorFactory = videoFrameProcessorFactory;
}
@Override
public PreviewingVideoGraph create(
Context context,
ColorInfo inputColorInfo,
ColorInfo outputColorInfo,
DebugViewProvider debugViewProvider,
Listener listener,
Executor listenerExecutor,
List<Effect> compositionEffects,
long initialTimestampOffsetUs) {
@Nullable Presentation presentation = null;
for (int i = 0; i < compositionEffects.size(); i++) {
Effect effect = compositionEffects.get(i);
if (effect instanceof Presentation) {
presentation = (Presentation) effect;
}
}
return new PreviewingSingleInputVideoGraph(
context,
videoFrameProcessorFactory,
inputColorInfo,
outputColorInfo,
debugViewProvider,
listener,
listenerExecutor,
presentation,
initialTimestampOffsetUs);
}
}
private PreviewingSingleInputVideoGraph(
Context context,
VideoFrameProcessor.Factory videoFrameProcessorFactory,
ColorInfo inputColorInfo,
ColorInfo outputColorInfo,
DebugViewProvider debugViewProvider,
Listener listener,
Executor listenerExecutor,
@Nullable Presentation presentation,
long initialTimestampOffsetUs) {
super(
context,
videoFrameProcessorFactory,
inputColorInfo,
outputColorInfo,
listener,
debugViewProvider,
listenerExecutor,
VideoCompositorSettings.DEFAULT,
// Previewing needs frame render timing.
/* renderFramesAutomatically= */ false,
presentation,
initialTimestampOffsetUs);
}
}

View file

@ -134,6 +134,8 @@ public abstract class SingleInputVideoGraph implements VideoGraph {
hasProducedFrameWithTimestampZero = true; hasProducedFrameWithTimestampZero = true;
} }
lastProcessedFramePresentationTimeUs = presentationTimeUs; lastProcessedFramePresentationTimeUs = presentationTimeUs;
listenerExecutor.execute(
() -> listener.onOutputFrameAvailableForRendering(presentationTimeUs));
} }
@Override @Override

View file

@ -26,6 +26,7 @@ import android.os.Handler;
import android.util.Pair; import android.util.Pair;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.ColorInfo; import androidx.media3.common.ColorInfo;
import androidx.media3.common.DebugViewProvider; import androidx.media3.common.DebugViewProvider;
@ -33,9 +34,11 @@ import androidx.media3.common.Effect;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.FrameInfo; import androidx.media3.common.FrameInfo;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.PreviewingVideoGraph;
import androidx.media3.common.SurfaceInfo; import androidx.media3.common.SurfaceInfo;
import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.VideoFrameProcessor; import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.VideoGraph;
import androidx.media3.common.VideoSize; import androidx.media3.common.VideoSize;
import androidx.media3.common.util.LongArrayQueue; import androidx.media3.common.util.LongArrayQueue;
import androidx.media3.common.util.Size; import androidx.media3.common.util.Size;
@ -43,6 +46,7 @@ import androidx.media3.common.util.TimedValueQueue;
import androidx.media3.common.util.TimestampIterator; import androidx.media3.common.util.TimestampIterator;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
@ -57,7 +61,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* package */ final class CompositingVideoSinkProvider implements VideoSinkProvider { /* package */ final class CompositingVideoSinkProvider implements VideoSinkProvider {
private final Context context; private final Context context;
private final VideoFrameProcessor.Factory videoFrameProcessorFactory; private final PreviewingVideoGraph.Factory previewingVideoGraphFactory;
private final VideoSink.RenderControl renderControl; private final VideoSink.RenderControl renderControl;
@Nullable private VideoSinkImpl videoSinkImpl; @Nullable private VideoSinkImpl videoSinkImpl;
@ -70,8 +74,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Context context, Context context,
VideoFrameProcessor.Factory videoFrameProcessorFactory, VideoFrameProcessor.Factory videoFrameProcessorFactory,
VideoSink.RenderControl renderControl) { VideoSink.RenderControl renderControl) {
this(
context,
new ReflectivePreviewingSingleInputVideoGraphFactory(videoFrameProcessorFactory),
renderControl);
}
@VisibleForTesting
/* package */ CompositingVideoSinkProvider(
Context context,
PreviewingVideoGraph.Factory previewingVideoGraphFactory,
VideoSink.RenderControl renderControl) {
this.context = context; this.context = context;
this.videoFrameProcessorFactory = videoFrameProcessorFactory; this.previewingVideoGraphFactory = previewingVideoGraphFactory;
this.renderControl = renderControl; this.renderControl = renderControl;
} }
@ -82,7 +97,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
try { try {
videoSinkImpl = videoSinkImpl =
new VideoSinkImpl(context, videoFrameProcessorFactory, renderControl, sourceFormat); new VideoSinkImpl(context, previewingVideoGraphFactory, renderControl, sourceFormat);
} catch (VideoFrameProcessingException e) { } catch (VideoFrameProcessingException e) {
throw new VideoSink.VideoSinkException(e, sourceFormat); throw new VideoSink.VideoSinkException(e, sourceFormat);
} }
@ -147,10 +162,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
private static final class VideoSinkImpl implements VideoSink, VideoFrameProcessor.Listener { private static final class VideoSinkImpl implements VideoSink, VideoGraph.Listener {
private final Context context; private final Context context;
private final RenderControl renderControl; private final VideoSink.RenderControl renderControl;
private final VideoFrameProcessor videoFrameProcessor; private final VideoFrameProcessor videoFrameProcessor;
private final LongArrayQueue processedFramesBufferTimestampsUs; private final LongArrayQueue processedFramesBufferTimestampsUs;
private final TimedValueQueue<Long> streamOffsets; private final TimedValueQueue<Long> streamOffsets;
@ -196,7 +211,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Creates a new instance. */ /** Creates a new instance. */
public VideoSinkImpl( public VideoSinkImpl(
Context context, Context context,
VideoFrameProcessor.Factory videoFrameProcessorFactory, PreviewingVideoGraph.Factory previewingVideoGraphFactory,
RenderControl renderControl, RenderControl renderControl,
Format sourceFormat) Format sourceFormat)
throws VideoFrameProcessingException { throws VideoFrameProcessingException {
@ -233,18 +248,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@SuppressWarnings("nullness:assignment") @SuppressWarnings("nullness:assignment")
@Initialized @Initialized
VideoSinkImpl thisRef = this; VideoSinkImpl thisRef = this;
videoFrameProcessor = PreviewingVideoGraph videoGraph =
videoFrameProcessorFactory.create( previewingVideoGraphFactory.create(
context, context,
DebugViewProvider.NONE,
inputColorInfo, inputColorInfo,
outputColorInfo, outputColorInfo,
/* renderFramesAutomatically= */ false, DebugViewProvider.NONE,
/* listener= */ thisRef,
/* listenerExecutor= */ handler::post, /* listenerExecutor= */ handler::post,
thisRef); /* compositionEffects= */ ImmutableList.of(),
/* initialTimestampOffsetUs= */ 0);
int videoGraphInputId = videoGraph.registerInput();
videoFrameProcessor = videoGraph.getProcessor(videoGraphInputId);
if (currentSurfaceAndSize != null) { if (currentSurfaceAndSize != null) {
Size outputSurfaceSize = currentSurfaceAndSize.second; Size outputSurfaceSize = currentSurfaceAndSize.second;
videoFrameProcessor.setOutputSurfaceInfo( videoGraph.setOutputSurfaceInfo(
new SurfaceInfo( new SurfaceInfo(
currentSurfaceAndSize.first, currentSurfaceAndSize.first,
outputSurfaceSize.getWidth(), outputSurfaceSize.getWidth(),
@ -399,14 +418,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.playbackSpeed = speed; this.playbackSpeed = speed;
} }
// VideoFrameProcessor.Listener impl
@Override
public void onInputStreamRegistered(
@VideoFrameProcessor.InputType int inputType, List<Effect> effects, FrameInfo frameInfo) {
// Do nothing.
}
@Override @Override
public void onOutputSizeChanged(int width, int height) { public void onOutputSizeChanged(int width, int height) {
VideoSize newVideoSize = new VideoSize(width, height); VideoSize newVideoSize = new VideoSize(width, height);
@ -454,12 +465,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
@Override @Override
public void onEnded() { public void onEnded(long finalFramePresentationTimeUs) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
// Other methods
public void release() { public void release() {
videoFrameProcessor.release(); videoFrameProcessor.release();
handler.removeCallbacksAndMessages(/* token= */ null); handler.removeCallbacksAndMessages(/* token= */ null);
@ -524,7 +533,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
outputSurface, outputResolution.getWidth(), outputResolution.getHeight())); outputSurface, outputResolution.getWidth(), outputResolution.getHeight()));
} }
/** Clears the set output surface info. */
public void clearOutputSurfaceInfo() { public void clearOutputSurfaceInfo() {
videoFrameProcessor.setOutputSurfaceInfo(null); videoFrameProcessor.setOutputSurfaceInfo(null);
currentSurfaceAndSize = null; currentSurfaceAndSize = null;
@ -616,4 +624,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
} }
/**
* Delays reflection for loading a {@linkplain PreviewingVideoGraph.Factory
* PreviewingSingleInputVideoGraph} instance.
*/
private static final class ReflectivePreviewingSingleInputVideoGraphFactory
implements PreviewingVideoGraph.Factory {
private final VideoFrameProcessor.Factory videoFrameProcessorFactory;
public ReflectivePreviewingSingleInputVideoGraphFactory(
VideoFrameProcessor.Factory videoFrameProcessorFactory) {
this.videoFrameProcessorFactory = videoFrameProcessorFactory;
}
@Override
public PreviewingVideoGraph create(
Context context,
ColorInfo inputColorInfo,
ColorInfo outputColorInfo,
DebugViewProvider debugViewProvider,
VideoGraph.Listener listener,
Executor listenerExecutor,
List<Effect> compositionEffects,
long initialTimestampOffsetUs)
throws VideoFrameProcessingException {
try {
Class<?> previewingSingleInputVideoGraphFactoryClass =
Class.forName("androidx.media3.effect.PreviewingSingleInputVideoGraph$Factory");
PreviewingVideoGraph.Factory factory =
(PreviewingVideoGraph.Factory)
previewingSingleInputVideoGraphFactoryClass
.getConstructor(VideoFrameProcessor.Factory.class)
.newInstance(videoFrameProcessorFactory);
return factory.create(
context,
inputColorInfo,
outputColorInfo,
debugViewProvider,
listener,
listenerExecutor,
compositionEffects,
initialTimestampOffsetUs);
} catch (Exception e) {
throw VideoFrameProcessingException.from(e);
}
}
}
} }

View file

@ -17,17 +17,21 @@ package androidx.media3.exoplayer.video;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.content.Context; import android.content.Context;
import androidx.media3.common.ColorInfo; import androidx.media3.common.ColorInfo;
import androidx.media3.common.DebugViewProvider; import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.Effect;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.PreviewingVideoGraph;
import androidx.media3.common.VideoFrameProcessor; import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.VideoGraph;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -128,29 +132,33 @@ public final class CompositingVideoSinkProviderTest {
} }
private static CompositingVideoSinkProvider createCompositingVideoSinkProvider() { private static CompositingVideoSinkProvider createCompositingVideoSinkProvider() {
VideoFrameProcessor.Factory factory = new TestVideoFrameProcessorFactory();
VideoSink.RenderControl renderControl = new TestRenderControl(); VideoSink.RenderControl renderControl = new TestRenderControl();
return new CompositingVideoSinkProvider( return new CompositingVideoSinkProvider(
ApplicationProvider.getApplicationContext(), factory, renderControl); ApplicationProvider.getApplicationContext(),
new TestPreviewingVideoGraphFactory(),
renderControl);
} }
private static class TestVideoFrameProcessorFactory implements VideoFrameProcessor.Factory { private static class TestPreviewingVideoGraphFactory implements PreviewingVideoGraph.Factory {
// Using a mock but we don't assert mock interactions. If needed to assert interactions, we // Using a mock but we don't assert mock interactions. If needed to assert interactions, we
// should a fake instead. // should a fake instead.
private final PreviewingVideoGraph previewingVideoGraph =
Mockito.mock(PreviewingVideoGraph.class);
private final VideoFrameProcessor videoFrameProcessor = Mockito.mock(VideoFrameProcessor.class); private final VideoFrameProcessor videoFrameProcessor = Mockito.mock(VideoFrameProcessor.class);
@Override @Override
public VideoFrameProcessor create( public PreviewingVideoGraph create(
Context context, Context context,
DebugViewProvider debugViewProvider,
ColorInfo inputColorInfo, ColorInfo inputColorInfo,
ColorInfo outputColorInfo, ColorInfo outputColorInfo,
boolean renderFramesAutomatically, DebugViewProvider debugViewProvider,
VideoGraph.Listener listener,
Executor listenerExecutor, Executor listenerExecutor,
VideoFrameProcessor.Listener listener) List<Effect> compositionEffects,
throws VideoFrameProcessingException { long initialTimestampOffsetUs) {
when(previewingVideoGraph.getProcessor(anyInt())).thenReturn(videoFrameProcessor);
when(videoFrameProcessor.registerInputFrame()).thenReturn(true); when(videoFrameProcessor.registerInputFrame()).thenReturn(true);
return videoFrameProcessor; return previewingVideoGraph;
} }
} }

View file

@ -513,6 +513,11 @@ import org.checkerframework.dataflow.qual.Pure;
setOutputSurfaceInfo(surfaceInfo); setOutputSurfaceInfo(surfaceInfo);
} }
@Override
public void onOutputFrameAvailableForRendering(long presentationTimeUs) {
// Do nothing.
}
@Override @Override
public void onEnded(long finalFramePresentationTimeUs) { public void onEnded(long finalFramePresentationTimeUs) {
VideoSampleExporter.this.finalFramePresentationTimeUs = finalFramePresentationTimeUs; VideoSampleExporter.this.finalFramePresentationTimeUs = finalFramePresentationTimeUs;