mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Experimental flag to limit the number of frames in encoder
Transformer.experimentalSetMaxFramesInEncoder controls max number of frames in encoder. VideoFrameProcessor now allows delayed releasing of frames to Surface, while still using the original presentation time. VideoSampleExporter can now configure video graphs to not render frames automatically to output surface. VideoSampleExporter.VideoGraphWrapper tracks how many frames are ready to be rendered to Surface, and how many frames are already in-use by encoder. PiperOrigin-RevId: 658429969
This commit is contained in:
parent
766902634e
commit
ddc86686b7
14 changed files with 298 additions and 32 deletions
|
|
@ -41,6 +41,9 @@ public final class SurfaceInfo {
|
||||||
*/
|
*/
|
||||||
public final int orientationDegrees;
|
public final int orientationDegrees;
|
||||||
|
|
||||||
|
/** Whether the {@link #surface} is an encoder input surface. */
|
||||||
|
public final boolean isEncoderInputSurface;
|
||||||
|
|
||||||
/** Creates a new instance. */
|
/** Creates a new instance. */
|
||||||
public SurfaceInfo(Surface surface, int width, int height) {
|
public SurfaceInfo(Surface surface, int width, int height) {
|
||||||
this(surface, width, height, /* orientationDegrees= */ 0);
|
this(surface, width, height, /* orientationDegrees= */ 0);
|
||||||
|
|
@ -48,6 +51,16 @@ public final class SurfaceInfo {
|
||||||
|
|
||||||
/** Creates a new instance. */
|
/** Creates a new instance. */
|
||||||
public SurfaceInfo(Surface surface, int width, int height, int orientationDegrees) {
|
public SurfaceInfo(Surface surface, int width, int height, int orientationDegrees) {
|
||||||
|
this(surface, width, height, orientationDegrees, /* isEncoderInputSurface= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a new instance. */
|
||||||
|
public SurfaceInfo(
|
||||||
|
Surface surface,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int orientationDegrees,
|
||||||
|
boolean isEncoderInputSurface) {
|
||||||
checkArgument(
|
checkArgument(
|
||||||
orientationDegrees == 0
|
orientationDegrees == 0
|
||||||
|| orientationDegrees == 90
|
|| orientationDegrees == 90
|
||||||
|
|
@ -58,6 +71,7 @@ public final class SurfaceInfo {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.orientationDegrees = orientationDegrees;
|
this.orientationDegrees = orientationDegrees;
|
||||||
|
this.isEncoderInputSurface = isEncoderInputSurface;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -72,6 +86,7 @@ public final class SurfaceInfo {
|
||||||
return width == that.width
|
return width == that.width
|
||||||
&& height == that.height
|
&& height == that.height
|
||||||
&& orientationDegrees == that.orientationDegrees
|
&& orientationDegrees == that.orientationDegrees
|
||||||
|
&& isEncoderInputSurface == that.isEncoderInputSurface
|
||||||
&& surface.equals(that.surface);
|
&& surface.equals(that.surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,6 +96,7 @@ public final class SurfaceInfo {
|
||||||
result = 31 * result + width;
|
result = 31 * result + width;
|
||||||
result = 31 * result + height;
|
result = 31 * result + height;
|
||||||
result = 31 * result + orientationDegrees;
|
result = 31 * result + orientationDegrees;
|
||||||
|
result = 31 * result + (isEncoderInputSurface ? 1 : 0);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,13 @@ public interface VideoFrameProcessor {
|
||||||
/** Indicates the frame should be dropped after {@link #renderOutputFrame(long)} is invoked. */
|
/** Indicates the frame should be dropped after {@link #renderOutputFrame(long)} is invoked. */
|
||||||
long DROP_OUTPUT_FRAME = -2;
|
long DROP_OUTPUT_FRAME = -2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates the frame should preserve the input presentation time when {@link
|
||||||
|
* #renderOutputFrame(long)} is invoked.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("GoodTime-ApiWithNumericTimeUnit") // This is a named constant, not a time unit.
|
||||||
|
long RENDER_OUTPUT_FRAME_WITH_PRESENTATION_TIME = -3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an input {@link Bitmap} to the {@link VideoFrameProcessor}.
|
* Provides an input {@link Bitmap} to the {@link VideoFrameProcessor}.
|
||||||
*
|
*
|
||||||
|
|
@ -333,7 +340,10 @@ public interface VideoFrameProcessor {
|
||||||
*
|
*
|
||||||
* @param renderTimeNs The render time to use for the frame, in nanoseconds. The render time can
|
* @param renderTimeNs The render time to use for the frame, in nanoseconds. The render time can
|
||||||
* be before or after the current system time. Use {@link #DROP_OUTPUT_FRAME} to drop the
|
* be before or after the current system time. Use {@link #DROP_OUTPUT_FRAME} to drop the
|
||||||
* frame, or {@link #RENDER_OUTPUT_FRAME_IMMEDIATELY} to render the frame immediately.
|
* frame, or {@link #RENDER_OUTPUT_FRAME_IMMEDIATELY} to render the frame immediately, or
|
||||||
|
* {@link #RENDER_OUTPUT_FRAME_WITH_PRESENTATION_TIME} to render the frame to the {@linkplain
|
||||||
|
* #setOutputSurfaceInfo output surface} with the presentation timestamp seen in {@link
|
||||||
|
* Listener#onOutputFrameAvailableForRendering(long)}.
|
||||||
*/
|
*/
|
||||||
void renderOutputFrame(long renderTimeNs);
|
void renderOutputFrame(long renderTimeNs);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package androidx.media3.effect;
|
package androidx.media3.effect;
|
||||||
|
|
||||||
|
import static androidx.media3.common.VideoFrameProcessor.RENDER_OUTPUT_FRAME_IMMEDIATELY;
|
||||||
|
import static androidx.media3.common.VideoFrameProcessor.RENDER_OUTPUT_FRAME_WITH_PRESENTATION_TIME;
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.common.util.Assertions.checkState;
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
import static androidx.media3.effect.DebugTraceUtil.COMPONENT_VFP;
|
import static androidx.media3.effect.DebugTraceUtil.COMPONENT_VFP;
|
||||||
|
|
@ -443,12 +445,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
GlUtil.clearFocusedBuffers();
|
GlUtil.clearFocusedBuffers();
|
||||||
defaultShaderProgram.drawFrame(inputTexture.texId, presentationTimeUs);
|
defaultShaderProgram.drawFrame(inputTexture.texId, presentationTimeUs);
|
||||||
|
|
||||||
EGLExt.eglPresentationTimeANDROID(
|
long eglPresentationTimeNs;
|
||||||
eglDisplay,
|
if (renderTimeNs == RENDER_OUTPUT_FRAME_IMMEDIATELY) {
|
||||||
outputEglSurface,
|
eglPresentationTimeNs = System.nanoTime();
|
||||||
renderTimeNs == VideoFrameProcessor.RENDER_OUTPUT_FRAME_IMMEDIATELY
|
} else if (renderTimeNs == RENDER_OUTPUT_FRAME_WITH_PRESENTATION_TIME) {
|
||||||
? System.nanoTime()
|
checkState(presentationTimeUs != C.TIME_UNSET);
|
||||||
: renderTimeNs);
|
eglPresentationTimeNs = presentationTimeUs * 1000;
|
||||||
|
} else {
|
||||||
|
eglPresentationTimeNs = renderTimeNs;
|
||||||
|
}
|
||||||
|
|
||||||
|
EGLExt.eglPresentationTimeANDROID(eglDisplay, outputEglSurface, eglPresentationTimeNs);
|
||||||
EGL14.eglSwapBuffers(eglDisplay, outputEglSurface);
|
EGL14.eglSwapBuffers(eglDisplay, outputEglSurface);
|
||||||
DebugTraceUtil.logEvent(COMPONENT_VFP, EVENT_RENDERED_TO_OUTPUT_SURFACE, presentationTimeUs);
|
DebugTraceUtil.logEvent(COMPONENT_VFP, EVENT_RENDERED_TO_OUTPUT_SURFACE, presentationTimeUs);
|
||||||
}
|
}
|
||||||
|
|
@ -524,8 +531,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
eglDisplay,
|
eglDisplay,
|
||||||
outputSurfaceInfo.surface,
|
outputSurfaceInfo.surface,
|
||||||
outputColorInfo.colorTransfer,
|
outputColorInfo.colorTransfer,
|
||||||
// Frames are only rendered automatically when outputting to an encoder.
|
outputSurfaceInfo.isEncoderInputSurface);
|
||||||
/* isEncoderInputSurface= */ renderFramesAutomatically);
|
|
||||||
}
|
}
|
||||||
if (textureOutputListener != null) {
|
if (textureOutputListener != null) {
|
||||||
outputTexturePool.ensureConfigured(glObjectsProvider, outputWidth, outputHeight);
|
outputTexturePool.ensureConfigured(glObjectsProvider, outputWidth, outputHeight);
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ public abstract class MultipleInputVideoGraph implements VideoGraph {
|
||||||
private final SparseArray<CompositorOutputTextureRelease> compositorOutputTextureReleases;
|
private final SparseArray<CompositorOutputTextureRelease> compositorOutputTextureReleases;
|
||||||
|
|
||||||
private final long initialTimestampOffsetUs;
|
private final long initialTimestampOffsetUs;
|
||||||
|
private final boolean renderFramesAutomatically;
|
||||||
|
|
||||||
@Nullable private VideoFrameProcessor compositionVideoFrameProcessor;
|
@Nullable private VideoFrameProcessor compositionVideoFrameProcessor;
|
||||||
@Nullable private VideoCompositor videoCompositor;
|
@Nullable private VideoCompositor videoCompositor;
|
||||||
|
|
@ -106,7 +107,8 @@ public abstract class MultipleInputVideoGraph implements VideoGraph {
|
||||||
Executor listenerExecutor,
|
Executor listenerExecutor,
|
||||||
VideoCompositorSettings videoCompositorSettings,
|
VideoCompositorSettings videoCompositorSettings,
|
||||||
List<Effect> compositionEffects,
|
List<Effect> compositionEffects,
|
||||||
long initialTimestampOffsetUs) {
|
long initialTimestampOffsetUs,
|
||||||
|
boolean renderFramesAutomatically) {
|
||||||
checkArgument(videoFrameProcessorFactory instanceof DefaultVideoFrameProcessor.Factory);
|
checkArgument(videoFrameProcessorFactory instanceof DefaultVideoFrameProcessor.Factory);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.outputColorInfo = outputColorInfo;
|
this.outputColorInfo = outputColorInfo;
|
||||||
|
|
@ -116,6 +118,7 @@ public abstract class MultipleInputVideoGraph implements VideoGraph {
|
||||||
this.videoCompositorSettings = videoCompositorSettings;
|
this.videoCompositorSettings = videoCompositorSettings;
|
||||||
this.compositionEffects = new ArrayList<>(compositionEffects);
|
this.compositionEffects = new ArrayList<>(compositionEffects);
|
||||||
this.initialTimestampOffsetUs = initialTimestampOffsetUs;
|
this.initialTimestampOffsetUs = initialTimestampOffsetUs;
|
||||||
|
this.renderFramesAutomatically = renderFramesAutomatically;
|
||||||
lastRenderedPresentationTimeUs = C.TIME_UNSET;
|
lastRenderedPresentationTimeUs = C.TIME_UNSET;
|
||||||
preProcessors = new SparseArray<>();
|
preProcessors = new SparseArray<>();
|
||||||
sharedExecutorService = newSingleThreadScheduledExecutor(SHARED_EXECUTOR_NAME);
|
sharedExecutorService = newSingleThreadScheduledExecutor(SHARED_EXECUTOR_NAME);
|
||||||
|
|
@ -150,7 +153,7 @@ public abstract class MultipleInputVideoGraph implements VideoGraph {
|
||||||
context,
|
context,
|
||||||
debugViewProvider,
|
debugViewProvider,
|
||||||
outputColorInfo,
|
outputColorInfo,
|
||||||
/* renderFramesAutomatically= */ true,
|
renderFramesAutomatically,
|
||||||
/* listenerExecutor= */ MoreExecutors.directExecutor(),
|
/* listenerExecutor= */ MoreExecutors.directExecutor(),
|
||||||
new VideoFrameProcessor.Listener() {
|
new VideoFrameProcessor.Listener() {
|
||||||
// All of this listener's methods are called on the sharedExecutorService.
|
// All of this listener's methods are called on the sharedExecutorService.
|
||||||
|
|
@ -174,6 +177,9 @@ public abstract class MultipleInputVideoGraph implements VideoGraph {
|
||||||
hasProducedFrameWithTimestampZero = true;
|
hasProducedFrameWithTimestampZero = true;
|
||||||
}
|
}
|
||||||
lastRenderedPresentationTimeUs = presentationTimeUs;
|
lastRenderedPresentationTimeUs = presentationTimeUs;
|
||||||
|
|
||||||
|
listenerExecutor.execute(
|
||||||
|
() -> listener.onOutputFrameAvailableForRendering(presentationTimeUs));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -312,6 +318,10 @@ public abstract class MultipleInputVideoGraph implements VideoGraph {
|
||||||
released = true;
|
released = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected VideoFrameProcessor getCompositionVideoFrameProcessor() {
|
||||||
|
return checkStateNotNull(compositionVideoFrameProcessor);
|
||||||
|
}
|
||||||
|
|
||||||
protected long getInitialTimestampOffsetUs() {
|
protected long getInitialTimestampOffsetUs() {
|
||||||
return initialTimestampOffsetUs;
|
return initialTimestampOffsetUs;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,7 @@ public final class AndroidTestUtil {
|
||||||
.setCodecs("avc1.64001F")
|
.setCodecs("avc1.64001F")
|
||||||
.build())
|
.build())
|
||||||
.setVideoDurationUs(1_024_000L)
|
.setVideoDurationUs(1_024_000L)
|
||||||
|
.setVideoFrameCount(30)
|
||||||
.setVideoTimestampsUs(
|
.setVideoTimestampsUs(
|
||||||
ImmutableList.of(
|
ImmutableList.of(
|
||||||
0L, 33_366L, 66_733L, 100_100L, 133_466L, 166_833L, 200_200L, 233_566L, 266_933L,
|
0L, 33_366L, 66_733L, 100_100L, 133_466L, 166_833L, 200_200L, 233_566L, 266_933L,
|
||||||
|
|
@ -390,6 +391,7 @@ public final class AndroidTestUtil {
|
||||||
.setFrameRate(30.00f)
|
.setFrameRate(30.00f)
|
||||||
.setCodecs("avc1.42C015")
|
.setCodecs("avc1.42C015")
|
||||||
.build())
|
.build())
|
||||||
|
.setVideoFrameCount(932)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
public static final AssetInfo MP4_ASSET_WITH_SHORTER_AUDIO =
|
public static final AssetInfo MP4_ASSET_WITH_SHORTER_AUDIO =
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,60 @@ public class TransformerEndToEndTest {
|
||||||
assertThat(new File(result.filePath).length()).isGreaterThan(0);
|
assertThat(new File(result.filePath).length()).isGreaterThan(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void videoEditing_withOneFrameInEncoder_completesWithConsistentFrameCount()
|
||||||
|
throws Exception {
|
||||||
|
assumeFormatsSupported(
|
||||||
|
context,
|
||||||
|
testId,
|
||||||
|
/* inputFormat= */ MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S.videoFormat,
|
||||||
|
/* outputFormat= */ MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S.videoFormat);
|
||||||
|
Transformer transformer =
|
||||||
|
new Transformer.Builder(context)
|
||||||
|
.setEncoderFactory(new AndroidTestUtil.ForceEncodeEncoderFactory(context))
|
||||||
|
.experimentalSetMaxFramesInEncoder(1)
|
||||||
|
.build();
|
||||||
|
MediaItem mediaItem =
|
||||||
|
MediaItem.fromUri(Uri.parse(MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S.uri));
|
||||||
|
EditedMediaItem editedMediaItem = new EditedMediaItem.Builder(mediaItem).build();
|
||||||
|
|
||||||
|
ExportTestResult result =
|
||||||
|
new TransformerAndroidTestRunner.Builder(context, transformer)
|
||||||
|
.build()
|
||||||
|
.run(testId, editedMediaItem);
|
||||||
|
|
||||||
|
assertThat(result.exportResult.videoFrameCount)
|
||||||
|
.isEqualTo(MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S.videoFrameCount);
|
||||||
|
assertThat(new File(result.filePath).length()).isGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void videoEditing_withMaxFramesInEncoder_completesWithConsistentFrameCount()
|
||||||
|
throws Exception {
|
||||||
|
assumeFormatsSupported(
|
||||||
|
context,
|
||||||
|
testId,
|
||||||
|
/* inputFormat= */ MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S.videoFormat,
|
||||||
|
/* outputFormat= */ MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S.videoFormat);
|
||||||
|
Transformer transformer =
|
||||||
|
new Transformer.Builder(context)
|
||||||
|
.setEncoderFactory(new AndroidTestUtil.ForceEncodeEncoderFactory(context))
|
||||||
|
.experimentalSetMaxFramesInEncoder(16)
|
||||||
|
.build();
|
||||||
|
MediaItem mediaItem =
|
||||||
|
MediaItem.fromUri(Uri.parse(MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S.uri));
|
||||||
|
EditedMediaItem editedMediaItem = new EditedMediaItem.Builder(mediaItem).build();
|
||||||
|
|
||||||
|
ExportTestResult result =
|
||||||
|
new TransformerAndroidTestRunner.Builder(context, transformer)
|
||||||
|
.build()
|
||||||
|
.run(testId, editedMediaItem);
|
||||||
|
|
||||||
|
assertThat(result.exportResult.videoFrameCount)
|
||||||
|
.isEqualTo(MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S.videoFrameCount);
|
||||||
|
assertThat(new File(result.filePath).length()).isGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: b/345483531 - Migrate this test to a Parameterized ImageSequence test.
|
// TODO: b/345483531 - Migrate this test to a Parameterized ImageSequence test.
|
||||||
@Test
|
@Test
|
||||||
public void videoEditing_withShortAlternatingImages_completesWithCorrectFrameCountAndDuration()
|
public void videoEditing_withShortAlternatingImages_completesWithCorrectFrameCountAndDuration()
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ import static com.google.common.truth.Truth.assertWithMessage;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.net.Uri;
|
||||||
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Effect;
|
import androidx.media3.common.Effect;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.util.Size;
|
import androidx.media3.common.util.Size;
|
||||||
|
|
@ -71,9 +73,15 @@ public final class TransformerMultiSequenceCompositionTest {
|
||||||
private static final int EXPORT_WIDTH = 360;
|
private static final int EXPORT_WIDTH = 360;
|
||||||
private static final int EXPORT_HEIGHT = 240;
|
private static final int EXPORT_HEIGHT = 240;
|
||||||
|
|
||||||
@Parameters(name = "{0}")
|
@Parameters(name = "{0},maxFramesInEncoder={1}")
|
||||||
public static ImmutableList<Boolean> workingColorSpaceLinear() {
|
public static ImmutableList<Object[]> parameters() {
|
||||||
return ImmutableList.of(false, true);
|
ImmutableList.Builder<Object[]> listBuilder = new ImmutableList.Builder<>();
|
||||||
|
for (Boolean workingColorSpaceLinear : new boolean[] {false, true}) {
|
||||||
|
for (Integer maxFramesInEncoder : new int[] {C.INDEX_UNSET, 1, 16}) {
|
||||||
|
listBuilder.add(new Object[] {workingColorSpaceLinear, maxFramesInEncoder});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listBuilder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Context context = ApplicationProvider.getApplicationContext();
|
private final Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
|
@ -81,7 +89,11 @@ public final class TransformerMultiSequenceCompositionTest {
|
||||||
|
|
||||||
private String testId;
|
private String testId;
|
||||||
|
|
||||||
@Parameter public boolean workingColorSpaceLinear;
|
@Parameter(0)
|
||||||
|
public boolean workingColorSpaceLinear;
|
||||||
|
|
||||||
|
@Parameter(1)
|
||||||
|
public int maxFramesInEncoder;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUpTestId() {
|
public void setUpTestId() {
|
||||||
|
|
@ -218,6 +230,33 @@ public final class TransformerMultiSequenceCompositionTest {
|
||||||
extractBitmapsFromVideo(context, checkNotNull(result.filePath)), testId);
|
extractBitmapsFromVideo(context, checkNotNull(result.filePath)), testId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void export_completesWithConsistentFrameCount() throws Exception {
|
||||||
|
assumeFormatsSupported(
|
||||||
|
context,
|
||||||
|
testId,
|
||||||
|
/* inputFormat= */ MP4_ASSET.videoFormat,
|
||||||
|
/* outputFormat= */ MP4_ASSET.videoFormat);
|
||||||
|
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_ASSET.uri));
|
||||||
|
ImmutableList<Effect> videoEffects = ImmutableList.of(Presentation.createForHeight(480));
|
||||||
|
Effects effects = new Effects(/* audioProcessors= */ ImmutableList.of(), videoEffects);
|
||||||
|
EditedMediaItem editedMediaItem =
|
||||||
|
new EditedMediaItem.Builder(mediaItem).setEffects(effects).build();
|
||||||
|
Composition composition =
|
||||||
|
new Composition.Builder(
|
||||||
|
new EditedMediaItemSequence(editedMediaItem),
|
||||||
|
new EditedMediaItemSequence(editedMediaItem))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ExportTestResult result =
|
||||||
|
new TransformerAndroidTestRunner.Builder(context, buildTransformer())
|
||||||
|
.build()
|
||||||
|
.run(testId, composition);
|
||||||
|
|
||||||
|
assertThat(result.exportResult.videoFrameCount).isEqualTo(MP4_ASSET.videoFrameCount);
|
||||||
|
assertThat(new File(result.filePath).length()).isGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
private Transformer buildTransformer() {
|
private Transformer buildTransformer() {
|
||||||
// Use linear color space for grayscale effects.
|
// Use linear color space for grayscale effects.
|
||||||
Transformer.Builder builder = new Transformer.Builder(context);
|
Transformer.Builder builder = new Transformer.Builder(context);
|
||||||
|
|
@ -227,6 +266,7 @@ public final class TransformerMultiSequenceCompositionTest {
|
||||||
.setSdrWorkingColorSpace(DefaultVideoFrameProcessor.WORKING_COLOR_SPACE_LINEAR)
|
.setSdrWorkingColorSpace(DefaultVideoFrameProcessor.WORKING_COLOR_SPACE_LINEAR)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
builder.experimentalSetMaxFramesInEncoder(maxFramesInEncoder);
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -278,7 +318,7 @@ public final class TransformerMultiSequenceCompositionTest {
|
||||||
Bitmap actualBitmap = actualBitmaps.get(i);
|
Bitmap actualBitmap = actualBitmaps.get(i);
|
||||||
maybeSaveTestBitmap(
|
maybeSaveTestBitmap(
|
||||||
testId, /* bitmapLabel= */ String.valueOf(i), actualBitmap, /* path= */ null);
|
testId, /* bitmapLabel= */ String.valueOf(i), actualBitmap, /* path= */ null);
|
||||||
String subTestId = testId + "_" + i;
|
String subTestId = testId.replaceAll(",maxFramesInEncoder=-?\\d+", "") + "_" + i;
|
||||||
Bitmap expectedBitmap =
|
Bitmap expectedBitmap =
|
||||||
readBitmap(Util.formatInvariant("%s/%s.png", PNG_ASSET_BASE_PATH, subTestId));
|
readBitmap(Util.formatInvariant("%s/%s.png", PNG_ASSET_BASE_PATH, subTestId));
|
||||||
float averagePixelAbsoluteDifference =
|
float averagePixelAbsoluteDifference =
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ public final class ExperimentalAnalyzerModeFactory {
|
||||||
return transformer
|
return transformer
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.experimentalSetTrimOptimizationEnabled(false)
|
.experimentalSetTrimOptimizationEnabled(false)
|
||||||
|
.experimentalSetMaxFramesInEncoder(C.INDEX_UNSET)
|
||||||
.setEncoderFactory(new DroppingEncoder.Factory(context))
|
.setEncoderFactory(new DroppingEncoder.Factory(context))
|
||||||
.setMaxDelayBetweenMuxerSamplesMs(C.TIME_UNSET)
|
.setMaxDelayBetweenMuxerSamplesMs(C.TIME_UNSET)
|
||||||
.setMuxerFactory(
|
.setMuxerFactory(
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ public final class Transformer {
|
||||||
private boolean trimOptimizationEnabled;
|
private boolean trimOptimizationEnabled;
|
||||||
private boolean fileStartsOnVideoFrameEnabled;
|
private boolean fileStartsOnVideoFrameEnabled;
|
||||||
private long maxDelayBetweenMuxerSamplesMs;
|
private long maxDelayBetweenMuxerSamplesMs;
|
||||||
|
private int maxFramesInEncoder;
|
||||||
private ListenerSet<Transformer.Listener> listeners;
|
private ListenerSet<Transformer.Listener> listeners;
|
||||||
private AssetLoader.@MonotonicNonNull Factory assetLoaderFactory;
|
private AssetLoader.@MonotonicNonNull Factory assetLoaderFactory;
|
||||||
private AudioMixer.Factory audioMixerFactory;
|
private AudioMixer.Factory audioMixerFactory;
|
||||||
|
|
@ -133,6 +134,7 @@ public final class Transformer {
|
||||||
public Builder(Context context) {
|
public Builder(Context context) {
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
maxDelayBetweenMuxerSamplesMs = DEFAULT_MAX_DELAY_BETWEEN_MUXER_SAMPLES_MS;
|
maxDelayBetweenMuxerSamplesMs = DEFAULT_MAX_DELAY_BETWEEN_MUXER_SAMPLES_MS;
|
||||||
|
maxFramesInEncoder = C.INDEX_UNSET;
|
||||||
audioProcessors = ImmutableList.of();
|
audioProcessors = ImmutableList.of();
|
||||||
videoEffects = ImmutableList.of();
|
videoEffects = ImmutableList.of();
|
||||||
audioMixerFactory = new DefaultAudioMixer.Factory();
|
audioMixerFactory = new DefaultAudioMixer.Factory();
|
||||||
|
|
@ -158,6 +160,7 @@ public final class Transformer {
|
||||||
this.trimOptimizationEnabled = transformer.trimOptimizationEnabled;
|
this.trimOptimizationEnabled = transformer.trimOptimizationEnabled;
|
||||||
this.fileStartsOnVideoFrameEnabled = transformer.fileStartsOnVideoFrameEnabled;
|
this.fileStartsOnVideoFrameEnabled = transformer.fileStartsOnVideoFrameEnabled;
|
||||||
this.maxDelayBetweenMuxerSamplesMs = transformer.maxDelayBetweenMuxerSamplesMs;
|
this.maxDelayBetweenMuxerSamplesMs = transformer.maxDelayBetweenMuxerSamplesMs;
|
||||||
|
this.maxFramesInEncoder = transformer.maxFramesInEncoder;
|
||||||
this.listeners = transformer.listeners;
|
this.listeners = transformer.listeners;
|
||||||
this.assetLoaderFactory = transformer.assetLoaderFactory;
|
this.assetLoaderFactory = transformer.assetLoaderFactory;
|
||||||
this.audioMixerFactory = transformer.audioMixerFactory;
|
this.audioMixerFactory = transformer.audioMixerFactory;
|
||||||
|
|
@ -333,6 +336,30 @@ public final class Transformer {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limits how many video frames can be processed at any time by the {@linkplain Codec encoder}.
|
||||||
|
*
|
||||||
|
* <p>A video frame starts encoding when it enters the {@linkplain Codec#getInputSurface()
|
||||||
|
* encoder input surface}, and finishes encoding when the corresponding {@linkplain
|
||||||
|
* Codec#releaseOutputBuffer encoder output buffer is released}.
|
||||||
|
*
|
||||||
|
* <p>The default value is {@link C#INDEX_UNSET}, which means no limit is enforced.
|
||||||
|
*
|
||||||
|
* <p>This method is experimental and will be renamed or removed in a future release.
|
||||||
|
*
|
||||||
|
* @param maxFramesInEncoder The maximum number of frames that the video encoder is allowed to
|
||||||
|
* process at a time, or {@link C#INDEX_UNSET} if no limit is enforced.
|
||||||
|
* @return This builder.
|
||||||
|
* @throws IllegalArgumentException If {@code maxFramesInEncoder} is not equal to {@link
|
||||||
|
* C#INDEX_UNSET} and is non-positive.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder experimentalSetMaxFramesInEncoder(int maxFramesInEncoder) {
|
||||||
|
checkArgument(maxFramesInEncoder > 0 || maxFramesInEncoder == C.INDEX_UNSET);
|
||||||
|
this.maxFramesInEncoder = maxFramesInEncoder;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether to ensure that the output file starts on a video frame.
|
* Sets whether to ensure that the output file starts on a video frame.
|
||||||
*
|
*
|
||||||
|
|
@ -592,6 +619,7 @@ public final class Transformer {
|
||||||
trimOptimizationEnabled,
|
trimOptimizationEnabled,
|
||||||
fileStartsOnVideoFrameEnabled,
|
fileStartsOnVideoFrameEnabled,
|
||||||
maxDelayBetweenMuxerSamplesMs,
|
maxDelayBetweenMuxerSamplesMs,
|
||||||
|
maxFramesInEncoder,
|
||||||
listeners,
|
listeners,
|
||||||
assetLoaderFactory,
|
assetLoaderFactory,
|
||||||
audioMixerFactory,
|
audioMixerFactory,
|
||||||
|
|
@ -844,6 +872,7 @@ public final class Transformer {
|
||||||
private final boolean trimOptimizationEnabled;
|
private final boolean trimOptimizationEnabled;
|
||||||
private final boolean fileStartsOnVideoFrameEnabled;
|
private final boolean fileStartsOnVideoFrameEnabled;
|
||||||
private final long maxDelayBetweenMuxerSamplesMs;
|
private final long maxDelayBetweenMuxerSamplesMs;
|
||||||
|
private final int maxFramesInEncoder;
|
||||||
|
|
||||||
private final ListenerSet<Transformer.Listener> listeners;
|
private final ListenerSet<Transformer.Listener> listeners;
|
||||||
@Nullable private final AssetLoader.Factory assetLoaderFactory;
|
@Nullable private final AssetLoader.Factory assetLoaderFactory;
|
||||||
|
|
@ -881,6 +910,7 @@ public final class Transformer {
|
||||||
boolean trimOptimizationEnabled,
|
boolean trimOptimizationEnabled,
|
||||||
boolean fileStartsOnVideoFrameEnabled,
|
boolean fileStartsOnVideoFrameEnabled,
|
||||||
long maxDelayBetweenMuxerSamplesMs,
|
long maxDelayBetweenMuxerSamplesMs,
|
||||||
|
int maxFramesInEncoder,
|
||||||
ListenerSet<Listener> listeners,
|
ListenerSet<Listener> listeners,
|
||||||
@Nullable AssetLoader.Factory assetLoaderFactory,
|
@Nullable AssetLoader.Factory assetLoaderFactory,
|
||||||
AudioMixer.Factory audioMixerFactory,
|
AudioMixer.Factory audioMixerFactory,
|
||||||
|
|
@ -901,6 +931,7 @@ public final class Transformer {
|
||||||
this.trimOptimizationEnabled = trimOptimizationEnabled;
|
this.trimOptimizationEnabled = trimOptimizationEnabled;
|
||||||
this.fileStartsOnVideoFrameEnabled = fileStartsOnVideoFrameEnabled;
|
this.fileStartsOnVideoFrameEnabled = fileStartsOnVideoFrameEnabled;
|
||||||
this.maxDelayBetweenMuxerSamplesMs = maxDelayBetweenMuxerSamplesMs;
|
this.maxDelayBetweenMuxerSamplesMs = maxDelayBetweenMuxerSamplesMs;
|
||||||
|
this.maxFramesInEncoder = maxFramesInEncoder;
|
||||||
this.listeners = listeners;
|
this.listeners = listeners;
|
||||||
this.assetLoaderFactory = assetLoaderFactory;
|
this.assetLoaderFactory = assetLoaderFactory;
|
||||||
this.audioMixerFactory = audioMixerFactory;
|
this.audioMixerFactory = audioMixerFactory;
|
||||||
|
|
@ -1611,6 +1642,7 @@ public final class Transformer {
|
||||||
audioMixerFactory,
|
audioMixerFactory,
|
||||||
videoFrameProcessorFactory,
|
videoFrameProcessorFactory,
|
||||||
encoderFactory,
|
encoderFactory,
|
||||||
|
maxFramesInEncoder,
|
||||||
muxerWrapper,
|
muxerWrapper,
|
||||||
componentListener,
|
componentListener,
|
||||||
fallbackListener,
|
fallbackListener,
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
private final Object setMaxSequenceDurationUsLock;
|
private final Object setMaxSequenceDurationUsLock;
|
||||||
private final Object progressLock;
|
private final Object progressLock;
|
||||||
private final ProgressHolder internalProgressHolder;
|
private final ProgressHolder internalProgressHolder;
|
||||||
|
private final int maxFramesInEncoder;
|
||||||
|
|
||||||
private boolean isDrainingExporters;
|
private boolean isDrainingExporters;
|
||||||
|
|
||||||
|
|
@ -192,6 +193,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
AudioMixer.Factory audioMixerFactory,
|
AudioMixer.Factory audioMixerFactory,
|
||||||
VideoFrameProcessor.Factory videoFrameProcessorFactory,
|
VideoFrameProcessor.Factory videoFrameProcessorFactory,
|
||||||
Codec.EncoderFactory encoderFactory,
|
Codec.EncoderFactory encoderFactory,
|
||||||
|
int maxFramesInEncoder,
|
||||||
MuxerWrapper muxerWrapper,
|
MuxerWrapper muxerWrapper,
|
||||||
Listener listener,
|
Listener listener,
|
||||||
FallbackListener fallbackListener,
|
FallbackListener fallbackListener,
|
||||||
|
|
@ -202,6 +204,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.composition = composition;
|
this.composition = composition;
|
||||||
this.encoderFactory = new CapturingEncoderFactory(encoderFactory);
|
this.encoderFactory = new CapturingEncoderFactory(encoderFactory);
|
||||||
|
this.maxFramesInEncoder = maxFramesInEncoder;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
this.applicationHandler = applicationHandler;
|
this.applicationHandler = applicationHandler;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
|
|
@ -738,8 +741,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
fallbackListener,
|
fallbackListener,
|
||||||
debugViewProvider,
|
debugViewProvider,
|
||||||
videoSampleTimestampOffsetUs,
|
videoSampleTimestampOffsetUs,
|
||||||
/* hasMultipleInputs= */ assetLoaderInputTracker
|
/* hasMultipleInputs= */ assetLoaderInputTracker.hasMultipleConcurrentVideoTracks(),
|
||||||
.hasMultipleConcurrentVideoTracks()));
|
maxFramesInEncoder));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package androidx.media3.transformer;
|
package androidx.media3.transformer;
|
||||||
|
|
||||||
|
import static androidx.media3.common.VideoFrameProcessor.RENDER_OUTPUT_FRAME_WITH_PRESENTATION_TIME;
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -53,7 +55,8 @@ import java.util.concurrent.Executor;
|
||||||
Executor listenerExecutor,
|
Executor listenerExecutor,
|
||||||
VideoCompositorSettings videoCompositorSettings,
|
VideoCompositorSettings videoCompositorSettings,
|
||||||
List<Effect> compositionEffects,
|
List<Effect> compositionEffects,
|
||||||
long initialTimestampOffsetUs) {
|
long initialTimestampOffsetUs,
|
||||||
|
boolean renderFramesAutomatically) {
|
||||||
return new TransformerMultipleInputVideoGraph(
|
return new TransformerMultipleInputVideoGraph(
|
||||||
context,
|
context,
|
||||||
videoFrameProcessorFactory,
|
videoFrameProcessorFactory,
|
||||||
|
|
@ -63,7 +66,8 @@ import java.util.concurrent.Executor;
|
||||||
listenerExecutor,
|
listenerExecutor,
|
||||||
videoCompositorSettings,
|
videoCompositorSettings,
|
||||||
compositionEffects,
|
compositionEffects,
|
||||||
initialTimestampOffsetUs);
|
initialTimestampOffsetUs,
|
||||||
|
renderFramesAutomatically);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +80,8 @@ import java.util.concurrent.Executor;
|
||||||
Executor listenerExecutor,
|
Executor listenerExecutor,
|
||||||
VideoCompositorSettings videoCompositorSettings,
|
VideoCompositorSettings videoCompositorSettings,
|
||||||
List<Effect> compositionEffects,
|
List<Effect> compositionEffects,
|
||||||
long initialTimestampOffsetUs) {
|
long initialTimestampOffsetUs,
|
||||||
|
boolean renderFramesAutomatically) {
|
||||||
super(
|
super(
|
||||||
context,
|
context,
|
||||||
videoFrameProcessorFactory,
|
videoFrameProcessorFactory,
|
||||||
|
|
@ -86,7 +91,8 @@ import java.util.concurrent.Executor;
|
||||||
listenerExecutor,
|
listenerExecutor,
|
||||||
videoCompositorSettings,
|
videoCompositorSettings,
|
||||||
compositionEffects,
|
compositionEffects,
|
||||||
initialTimestampOffsetUs);
|
initialTimestampOffsetUs,
|
||||||
|
renderFramesAutomatically);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -95,4 +101,10 @@ import java.util.concurrent.Executor;
|
||||||
return new VideoFrameProcessingWrapper(
|
return new VideoFrameProcessingWrapper(
|
||||||
getProcessor(inputIndex), /* presentation= */ null, getInitialTimestampOffsetUs());
|
getProcessor(inputIndex), /* presentation= */ null, getInitialTimestampOffsetUs());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderOutputFrameWithMediaPresentationTime() {
|
||||||
|
getCompositionVideoFrameProcessor()
|
||||||
|
.renderOutputFrame(RENDER_OUTPUT_FRAME_WITH_PRESENTATION_TIME);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package androidx.media3.transformer;
|
package androidx.media3.transformer;
|
||||||
|
|
||||||
|
import static androidx.media3.common.VideoFrameProcessor.RENDER_OUTPUT_FRAME_WITH_PRESENTATION_TIME;
|
||||||
import static androidx.media3.common.util.Assertions.checkState;
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
@ -57,7 +58,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
Executor listenerExecutor,
|
Executor listenerExecutor,
|
||||||
VideoCompositorSettings videoCompositorSettings,
|
VideoCompositorSettings videoCompositorSettings,
|
||||||
List<Effect> compositionEffects,
|
List<Effect> compositionEffects,
|
||||||
long initialTimestampOffsetUs) {
|
long initialTimestampOffsetUs,
|
||||||
|
boolean renderFramesAutomatically) {
|
||||||
@Nullable Presentation presentation = null;
|
@Nullable Presentation presentation = null;
|
||||||
for (int i = 0; i < compositionEffects.size(); i++) {
|
for (int i = 0; i < compositionEffects.size(); i++) {
|
||||||
Effect effect = compositionEffects.get(i);
|
Effect effect = compositionEffects.get(i);
|
||||||
|
|
@ -73,7 +75,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
debugViewProvider,
|
debugViewProvider,
|
||||||
listenerExecutor,
|
listenerExecutor,
|
||||||
videoCompositorSettings,
|
videoCompositorSettings,
|
||||||
/* renderFramesAutomatically= */ true,
|
renderFramesAutomatically,
|
||||||
presentation,
|
presentation,
|
||||||
initialTimestampOffsetUs);
|
initialTimestampOffsetUs);
|
||||||
}
|
}
|
||||||
|
|
@ -114,4 +116,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
getProcessor(inputIndex), getPresentation(), getInitialTimestampOffsetUs());
|
getProcessor(inputIndex), getPresentation(), getInitialTimestampOffsetUs());
|
||||||
return videoFrameProcessingWrapper;
|
return videoFrameProcessingWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderOutputFrameWithMediaPresentationTime() {
|
||||||
|
getProcessor(getInputIndex()).renderOutputFrame(RENDER_OUTPUT_FRAME_WITH_PRESENTATION_TIME);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ 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.Effect;
|
||||||
|
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.VideoGraph;
|
||||||
|
|
@ -44,6 +45,10 @@ import java.util.concurrent.Executor;
|
||||||
* composition.
|
* composition.
|
||||||
* @param compositionEffects A list of {@linkplain Effect effects} to apply to the composition.
|
* @param compositionEffects A list of {@linkplain Effect effects} to apply to the composition.
|
||||||
* @param initialTimestampOffsetUs The timestamp offset for the first frame, in microseconds.
|
* @param initialTimestampOffsetUs The timestamp offset for the first frame, in microseconds.
|
||||||
|
* @param renderFramesAutomatically If {@code true}, the instance will render output frames to
|
||||||
|
* the {@linkplain #setOutputSurfaceInfo(SurfaceInfo) output surface} automatically as the
|
||||||
|
* instance is done processing them. If {@code false}, the instance will block until {@link
|
||||||
|
* #renderOutputFrameWithMediaPresentationTime()} is called, to render the frame.
|
||||||
* @return A new instance.
|
* @return A new instance.
|
||||||
* @throws VideoFrameProcessingException If a problem occurs while creating the {@link
|
* @throws VideoFrameProcessingException If a problem occurs while creating the {@link
|
||||||
* VideoFrameProcessor}.
|
* VideoFrameProcessor}.
|
||||||
|
|
@ -56,7 +61,8 @@ import java.util.concurrent.Executor;
|
||||||
Executor listenerExecutor,
|
Executor listenerExecutor,
|
||||||
VideoCompositorSettings videoCompositorSettings,
|
VideoCompositorSettings videoCompositorSettings,
|
||||||
List<Effect> compositionEffects,
|
List<Effect> compositionEffects,
|
||||||
long initialTimestampOffsetUs)
|
long initialTimestampOffsetUs,
|
||||||
|
boolean renderFramesAutomatically)
|
||||||
throws VideoFrameProcessingException;
|
throws VideoFrameProcessingException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,4 +79,18 @@ import java.util.concurrent.Executor;
|
||||||
* @param inputIndex The index of the input, which could be used to order the inputs.
|
* @param inputIndex The index of the input, which could be used to order the inputs.
|
||||||
*/
|
*/
|
||||||
GraphInput createInput(int inputIndex) throws VideoFrameProcessingException;
|
GraphInput createInput(int inputIndex) throws VideoFrameProcessingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the oldest unrendered output frame that has become {@linkplain
|
||||||
|
* Listener#onOutputFrameAvailableForRendering(long) available for rendering} to the output
|
||||||
|
* surface.
|
||||||
|
*
|
||||||
|
* <p>This method must only be called if {@code renderFramesAutomatically} was set to {@code
|
||||||
|
* false} using the {@link Factory} and should be called exactly once for each frame that becomes
|
||||||
|
* {@linkplain Listener#onOutputFrameAvailableForRendering(long) available for rendering}.
|
||||||
|
*
|
||||||
|
* <p>This will render the output frame to the {@linkplain #setOutputSurfaceInfo output surface}
|
||||||
|
* with the presentation seen in {@link Listener#onOutputFrameAvailableForRendering(long)}.
|
||||||
|
*/
|
||||||
|
void renderOutputFrameWithMediaPresentationTime();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import static androidx.media3.common.ColorInfo.SRGB_BT709_FULL;
|
||||||
import static androidx.media3.common.ColorInfo.isTransferHdr;
|
import static androidx.media3.common.ColorInfo.isTransferHdr;
|
||||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
import static androidx.media3.transformer.Composition.HDR_MODE_KEEP_HDR;
|
import static androidx.media3.transformer.Composition.HDR_MODE_KEEP_HDR;
|
||||||
import static androidx.media3.transformer.Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL;
|
import static androidx.media3.transformer.Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL;
|
||||||
import static androidx.media3.transformer.TransformerUtil.getOutputMimeTypeAndHdrModeAfterFallback;
|
import static androidx.media3.transformer.TransformerUtil.getOutputMimeTypeAndHdrModeAfterFallback;
|
||||||
|
|
@ -54,13 +55,14 @@ import java.nio.ByteBuffer;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import org.checkerframework.checker.initialization.qual.Initialized;
|
import org.checkerframework.checker.initialization.qual.Initialized;
|
||||||
|
import org.checkerframework.checker.lock.qual.GuardedBy;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.checkerframework.dataflow.qual.Pure;
|
import org.checkerframework.dataflow.qual.Pure;
|
||||||
|
|
||||||
/** Processes, encodes and muxes raw video frames. */
|
/** Processes, encodes and muxes raw video frames. */
|
||||||
/* package */ final class VideoSampleExporter extends SampleExporter {
|
/* package */ final class VideoSampleExporter extends SampleExporter {
|
||||||
|
|
||||||
private final TransformerVideoGraph videoGraph;
|
private final VideoGraphWrapper videoGraph;
|
||||||
private final EncoderWrapper encoderWrapper;
|
private final EncoderWrapper encoderWrapper;
|
||||||
private final DecoderInputBuffer encoderOutputBuffer;
|
private final DecoderInputBuffer encoderOutputBuffer;
|
||||||
private final long initialTimestampOffsetUs;
|
private final long initialTimestampOffsetUs;
|
||||||
|
|
@ -86,7 +88,8 @@ import org.checkerframework.dataflow.qual.Pure;
|
||||||
FallbackListener fallbackListener,
|
FallbackListener fallbackListener,
|
||||||
DebugViewProvider debugViewProvider,
|
DebugViewProvider debugViewProvider,
|
||||||
long initialTimestampOffsetUs,
|
long initialTimestampOffsetUs,
|
||||||
boolean hasMultipleInputs)
|
boolean hasMultipleInputs,
|
||||||
|
int maxFramesInEncoder)
|
||||||
throws ExportException {
|
throws ExportException {
|
||||||
// TODO(b/278259383) Consider delaying configuration of VideoSampleExporter to use the decoder
|
// TODO(b/278259383) Consider delaying configuration of VideoSampleExporter to use the decoder
|
||||||
// output format instead of the extractor output format, to match AudioSampleExporter behavior.
|
// output format instead of the extractor output format, to match AudioSampleExporter behavior.
|
||||||
|
|
@ -142,7 +145,8 @@ import org.checkerframework.dataflow.qual.Pure;
|
||||||
errorConsumer,
|
errorConsumer,
|
||||||
debugViewProvider,
|
debugViewProvider,
|
||||||
videoCompositorSettings,
|
videoCompositorSettings,
|
||||||
compositionEffects);
|
compositionEffects,
|
||||||
|
maxFramesInEncoder);
|
||||||
videoGraph.initialize();
|
videoGraph.initialize();
|
||||||
} catch (VideoFrameProcessingException e) {
|
} catch (VideoFrameProcessingException e) {
|
||||||
throw ExportException.createForVideoFrameProcessingException(e);
|
throw ExportException.createForVideoFrameProcessingException(e);
|
||||||
|
|
@ -199,6 +203,7 @@ import org.checkerframework.dataflow.qual.Pure;
|
||||||
@Override
|
@Override
|
||||||
protected void releaseMuxerInputBuffer() throws ExportException {
|
protected void releaseMuxerInputBuffer() throws ExportException {
|
||||||
encoderWrapper.releaseOutputBuffer(/* render= */ false);
|
encoderWrapper.releaseOutputBuffer(/* render= */ false);
|
||||||
|
videoGraph.onEncoderBufferReleased();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -340,7 +345,8 @@ import org.checkerframework.dataflow.qual.Pure;
|
||||||
encoder.getInputSurface(),
|
encoder.getInputSurface(),
|
||||||
actualEncoderFormat.width,
|
actualEncoderFormat.width,
|
||||||
actualEncoderFormat.height,
|
actualEncoderFormat.height,
|
||||||
outputRotationDegrees);
|
outputRotationDegrees,
|
||||||
|
/* isEncoderInputSurface= */ true);
|
||||||
|
|
||||||
if (releaseEncoder) {
|
if (releaseEncoder) {
|
||||||
encoder.release();
|
encoder.release();
|
||||||
|
|
@ -454,6 +460,12 @@ import org.checkerframework.dataflow.qual.Pure;
|
||||||
|
|
||||||
private final TransformerVideoGraph videoGraph;
|
private final TransformerVideoGraph videoGraph;
|
||||||
private final Consumer<ExportException> errorConsumer;
|
private final Consumer<ExportException> errorConsumer;
|
||||||
|
private final int maxFramesInEncoder;
|
||||||
|
private final boolean renderFramesAutomatically;
|
||||||
|
private final Object lock;
|
||||||
|
|
||||||
|
private @GuardedBy("lock") int framesInEncoder;
|
||||||
|
private @GuardedBy("lock") int framesAvailableToRender;
|
||||||
|
|
||||||
public VideoGraphWrapper(
|
public VideoGraphWrapper(
|
||||||
Context context,
|
Context context,
|
||||||
|
|
@ -462,7 +474,8 @@ import org.checkerframework.dataflow.qual.Pure;
|
||||||
Consumer<ExportException> errorConsumer,
|
Consumer<ExportException> errorConsumer,
|
||||||
DebugViewProvider debugViewProvider,
|
DebugViewProvider debugViewProvider,
|
||||||
VideoCompositorSettings videoCompositorSettings,
|
VideoCompositorSettings videoCompositorSettings,
|
||||||
List<Effect> compositionEffects)
|
List<Effect> compositionEffects,
|
||||||
|
int maxFramesInEncoder)
|
||||||
throws VideoFrameProcessingException {
|
throws VideoFrameProcessingException {
|
||||||
this.errorConsumer = errorConsumer;
|
this.errorConsumer = errorConsumer;
|
||||||
// To satisfy the nullness checker by declaring an initialized this reference used in the
|
// To satisfy the nullness checker by declaring an initialized this reference used in the
|
||||||
|
|
@ -470,6 +483,11 @@ import org.checkerframework.dataflow.qual.Pure;
|
||||||
@SuppressWarnings("nullness:assignment")
|
@SuppressWarnings("nullness:assignment")
|
||||||
@Initialized
|
@Initialized
|
||||||
VideoGraphWrapper thisRef = this;
|
VideoGraphWrapper thisRef = this;
|
||||||
|
this.maxFramesInEncoder = maxFramesInEncoder;
|
||||||
|
// Automatically render frames if the sample exporter does not limit the number of frames in
|
||||||
|
// the encoder.
|
||||||
|
renderFramesAutomatically = maxFramesInEncoder < 1;
|
||||||
|
lock = new Object();
|
||||||
videoGraph =
|
videoGraph =
|
||||||
videoGraphFactory.create(
|
videoGraphFactory.create(
|
||||||
context,
|
context,
|
||||||
|
|
@ -479,7 +497,8 @@ import org.checkerframework.dataflow.qual.Pure;
|
||||||
/* listenerExecutor= */ MoreExecutors.directExecutor(),
|
/* listenerExecutor= */ MoreExecutors.directExecutor(),
|
||||||
videoCompositorSettings,
|
videoCompositorSettings,
|
||||||
compositionEffects,
|
compositionEffects,
|
||||||
initialTimestampOffsetUs);
|
initialTimestampOffsetUs,
|
||||||
|
renderFramesAutomatically);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -495,7 +514,12 @@ import org.checkerframework.dataflow.qual.Pure;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOutputFrameAvailableForRendering(long framePresentationTimeUs) {
|
public void onOutputFrameAvailableForRendering(long framePresentationTimeUs) {
|
||||||
// Do nothing.
|
if (!renderFramesAutomatically) {
|
||||||
|
synchronized (lock) {
|
||||||
|
framesAvailableToRender += 1;
|
||||||
|
}
|
||||||
|
maybeRenderEarliestOutputFrame();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -534,6 +558,11 @@ import org.checkerframework.dataflow.qual.Pure;
|
||||||
return videoGraph.createInput(inputIndex);
|
return videoGraph.createInput(inputIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderOutputFrameWithMediaPresentationTime() {
|
||||||
|
videoGraph.renderOutputFrameWithMediaPresentationTime();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setOutputSurfaceInfo(@Nullable SurfaceInfo outputSurfaceInfo) {
|
public void setOutputSurfaceInfo(@Nullable SurfaceInfo outputSurfaceInfo) {
|
||||||
videoGraph.setOutputSurfaceInfo(outputSurfaceInfo);
|
videoGraph.setOutputSurfaceInfo(outputSurfaceInfo);
|
||||||
|
|
@ -548,5 +577,29 @@ import org.checkerframework.dataflow.qual.Pure;
|
||||||
public void release() {
|
public void release() {
|
||||||
videoGraph.release();
|
videoGraph.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onEncoderBufferReleased() {
|
||||||
|
if (!renderFramesAutomatically) {
|
||||||
|
synchronized (lock) {
|
||||||
|
checkState(framesInEncoder > 0);
|
||||||
|
framesInEncoder -= 1;
|
||||||
|
}
|
||||||
|
maybeRenderEarliestOutputFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeRenderEarliestOutputFrame() {
|
||||||
|
boolean shouldRender = false;
|
||||||
|
synchronized (lock) {
|
||||||
|
if (framesAvailableToRender > 0 && framesInEncoder < maxFramesInEncoder) {
|
||||||
|
framesInEncoder += 1;
|
||||||
|
framesAvailableToRender -= 1;
|
||||||
|
shouldRender = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldRender) {
|
||||||
|
renderOutputFrameWithMediaPresentationTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue