-
Notifications
You must be signed in to change notification settings - Fork 409
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a thumbnail strip effect that tiles frames horizontally.
The size of the thumbnail strip and the timestamp of the video frames to use must be specified by the user of the effect. PiperOrigin-RevId: 552809210
- Loading branch information
Showing
7 changed files
with
422 additions
and
1 deletion.
There are no files selected for viewing
169 changes: 169 additions & 0 deletions
169
...ies/effect/src/androidTest/java/androidx/media3/effect/ThumbnailStripEffectPixelTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
/* | ||
* 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 static androidx.media3.common.util.Assertions.checkNotNull; | ||
import static androidx.media3.test.utils.BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE; | ||
import static androidx.media3.test.utils.BitmapPixelTestUtil.createArgb8888BitmapFromFocusedGlFramebuffer; | ||
import static androidx.media3.test.utils.BitmapPixelTestUtil.createArgb8888BitmapWithSolidColor; | ||
import static androidx.media3.test.utils.BitmapPixelTestUtil.createGlTextureFromBitmap; | ||
import static androidx.media3.test.utils.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888; | ||
import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap; | ||
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap; | ||
import static androidx.test.core.app.ApplicationProvider.getApplicationContext; | ||
import static com.google.common.truth.Truth.assertThat; | ||
|
||
import android.content.Context; | ||
import android.graphics.Bitmap; | ||
import android.graphics.Color; | ||
import android.opengl.EGLContext; | ||
import android.opengl.EGLDisplay; | ||
import android.opengl.EGLSurface; | ||
import androidx.media3.common.VideoFrameProcessingException; | ||
import androidx.media3.common.util.GlUtil; | ||
import androidx.media3.common.util.Util; | ||
import androidx.test.ext.junit.runners.AndroidJUnit4; | ||
import com.google.common.collect.ImmutableList; | ||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; | ||
import org.junit.After; | ||
import org.junit.Before; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
|
||
/** Pixel tests for {@link ThumbnailStripEffect}. */ | ||
@RunWith(AndroidJUnit4.class) | ||
public final class ThumbnailStripEffectPixelTest { | ||
private static final String ORIGINAL_PNG_ASSET_PATH = | ||
"media/bitmap/sample_mp4_first_frame/linear_colors/original.png"; | ||
private static final String TWO_THUMBNAILS_STRIP_PNG_ASSET_PATH = | ||
"media/bitmap/sample_mp4_first_frame/linear_colors/two_thumbnails_strip.png"; | ||
|
||
private final Context context = getApplicationContext(); | ||
|
||
private @MonotonicNonNull EGLDisplay eglDisplay; | ||
private @MonotonicNonNull EGLContext eglContext; | ||
private @MonotonicNonNull EGLSurface placeholderEglSurface; | ||
private @MonotonicNonNull ThumbnailStripShaderProgram thumbnailStripShaderProgram; | ||
private int inputTexId; | ||
private int inputWidth; | ||
private int inputHeight; | ||
|
||
@Before | ||
public void setUp() throws Exception { | ||
eglDisplay = GlUtil.getDefaultEglDisplay(); | ||
eglContext = GlUtil.createEglContext(eglDisplay); | ||
placeholderEglSurface = GlUtil.createFocusedPlaceholderEglSurface(eglContext, eglDisplay); | ||
|
||
Bitmap inputBitmap = readBitmap(ORIGINAL_PNG_ASSET_PATH); | ||
inputWidth = inputBitmap.getWidth(); | ||
inputHeight = inputBitmap.getHeight(); | ||
inputTexId = createGlTextureFromBitmap(inputBitmap); | ||
|
||
int outputTexId = | ||
GlUtil.createTexture(inputWidth, inputHeight, /* useHighPrecisionColorComponents= */ false); | ||
int frameBuffer = GlUtil.createFboForTexture(outputTexId); | ||
GlUtil.focusFramebuffer( | ||
checkNotNull(eglDisplay), | ||
checkNotNull(eglContext), | ||
checkNotNull(placeholderEglSurface), | ||
frameBuffer, | ||
inputWidth, | ||
inputHeight); | ||
} | ||
|
||
@After | ||
public void tearDown() throws GlUtil.GlException, VideoFrameProcessingException { | ||
if (thumbnailStripShaderProgram != null) { | ||
thumbnailStripShaderProgram.release(); | ||
} | ||
GlUtil.destroyEglContext(eglDisplay, eglContext); | ||
} | ||
|
||
@Test | ||
public void drawFrame_withOneTimestampAndOriginalSize_producesOriginalFrame() throws Exception { | ||
String testId = "drawFrame_withOneTimestampAndOriginalSize"; | ||
ThumbnailStripEffect thumbnailStripEffect = new ThumbnailStripEffect(inputWidth, inputHeight); | ||
thumbnailStripEffect.setTimestampsMs(ImmutableList.of(0L)); | ||
thumbnailStripShaderProgram = | ||
thumbnailStripEffect.toGlShaderProgram(context, /* useHdr= */ false); | ||
Bitmap expectedBitmap = readBitmap(ORIGINAL_PNG_ASSET_PATH); | ||
|
||
thumbnailStripShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0L); | ||
Bitmap actualBitmap = createArgb8888BitmapFromFocusedGlFramebuffer(inputWidth, inputHeight); | ||
|
||
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null); | ||
float averagePixelAbsoluteDifference = | ||
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId); | ||
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); | ||
} | ||
|
||
@Test | ||
public void drawFrame_zeroTimestamps_producesEmptyFrame() throws Exception { | ||
String testId = "drawFrame_zeroTimestamps"; | ||
ThumbnailStripEffect thumbnailStripEffect = new ThumbnailStripEffect(inputWidth, inputHeight); | ||
thumbnailStripEffect.setTimestampsMs(ImmutableList.of()); | ||
thumbnailStripShaderProgram = | ||
thumbnailStripEffect.toGlShaderProgram(context, /* useHdr= */ false); | ||
Bitmap expectedBitmap = | ||
createArgb8888BitmapWithSolidColor(inputWidth, inputHeight, Color.TRANSPARENT); | ||
|
||
thumbnailStripShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0L); | ||
Bitmap actualBitmap = createArgb8888BitmapFromFocusedGlFramebuffer(inputWidth, inputHeight); | ||
|
||
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null); | ||
float averagePixelAbsoluteDifference = | ||
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId); | ||
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); | ||
} | ||
|
||
@Test | ||
public void drawFrame_lateTimestamp_producesEmptyFrame() throws Exception { | ||
String testId = "drawFrame_lateTimestamp"; | ||
ThumbnailStripEffect thumbnailStripEffect = new ThumbnailStripEffect(inputWidth, inputHeight); | ||
thumbnailStripEffect.setTimestampsMs(ImmutableList.of(1L)); | ||
thumbnailStripShaderProgram = | ||
thumbnailStripEffect.toGlShaderProgram(context, /* useHdr= */ false); | ||
Bitmap expectedBitmap = | ||
createArgb8888BitmapWithSolidColor(inputWidth, inputHeight, Color.TRANSPARENT); | ||
|
||
thumbnailStripShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0L); | ||
Bitmap actualBitmap = createArgb8888BitmapFromFocusedGlFramebuffer(inputWidth, inputHeight); | ||
|
||
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null); | ||
float averagePixelAbsoluteDifference = | ||
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId); | ||
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); | ||
} | ||
|
||
@Test | ||
public void drawFrame_twoTimestamps_producesStrip() throws Exception { | ||
String testId = "drawFrame_twoTimestamps"; | ||
ThumbnailStripEffect thumbnailStripEffect = new ThumbnailStripEffect(inputWidth, inputHeight); | ||
thumbnailStripEffect.setTimestampsMs(ImmutableList.of(0L, 1L)); | ||
thumbnailStripShaderProgram = | ||
thumbnailStripEffect.toGlShaderProgram(context, /* useHdr= */ false); | ||
Bitmap expectedBitmap = readBitmap(TWO_THUMBNAILS_STRIP_PNG_ASSET_PATH); | ||
|
||
thumbnailStripShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0L); | ||
thumbnailStripShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ Util.msToUs(1L)); | ||
Bitmap actualBitmap = createArgb8888BitmapFromFocusedGlFramebuffer(inputWidth, inputHeight); | ||
|
||
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null); | ||
float averagePixelAbsoluteDifference = | ||
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId); | ||
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
libraries/effect/src/main/assets/shaders/fragment_shader_copy_es2.glsl
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
#version 100 | ||
// 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 | ||
// | ||
// 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. | ||
|
||
// ES 2 fragment shader that samples from a (non-external) texture with | ||
// uTexSampler. | ||
|
||
precision mediump float; | ||
uniform sampler2D uTexSampler; | ||
varying vec2 vTexSamplingCoord; | ||
|
||
void main() { | ||
vec3 src = texture2D(uTexSampler, vTexSamplingCoord).xyz; | ||
gl_FragColor = vec4(src, 1.0); | ||
} |
35 changes: 35 additions & 0 deletions
35
libraries/effect/src/main/assets/shaders/vertex_shader_thumbnail_strip_es2.glsl
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
#version 100 | ||
// 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 | ||
// | ||
// 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. | ||
|
||
// ES2 vertex shader that tiles frames horizontally. | ||
|
||
attribute vec4 aFramePosition; | ||
uniform int uIndex; | ||
uniform int uCount; | ||
varying vec2 vTexSamplingCoord; | ||
|
||
void main() { | ||
// Translate the coordinates from -1,+1 to 0,+2. | ||
float x = aFramePosition.x + 1.0; | ||
// Offset the frame by its index times its width (2). | ||
x += float(uIndex) * 2.0; | ||
// Shrink the frame to fit the thumbnail strip. | ||
x /= float(uCount); | ||
// Translate the coordinates back to -1,+1. | ||
x -= 1.0; | ||
|
||
gl_Position = vec4(x, aFramePosition.yzw); | ||
vTexSamplingCoord = aFramePosition.xy * 0.5 + 0.5; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
libraries/effect/src/main/java/androidx/media3/effect/ThumbnailStripEffect.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* | ||
* 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.media3.common.C; | ||
import androidx.media3.common.VideoFrameProcessingException; | ||
import androidx.media3.common.util.UnstableApi; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
/** | ||
* Generate a thumbnail strip (i.e. tile frames horizontally) containing frames at given {@link | ||
* #setTimestampsMs timestamps}. | ||
*/ | ||
@UnstableApi | ||
/* package */ final class ThumbnailStripEffect implements GlEffect { | ||
|
||
/* package */ final int stripWidth; | ||
/* package */ final int stripHeight; | ||
private final List<Long> timestampsMs; | ||
private int currentThumbnailIndex; | ||
|
||
/** | ||
* Creates a new instance with the given size. No thumbnails are drawn by default, call {@link | ||
* #setTimestampsMs} to change how many to draw and their timestamp. | ||
* | ||
* @param stripWidth The width of the thumbnail strip. | ||
* @param stripHeight The height of the thumbnail strip. | ||
*/ | ||
public ThumbnailStripEffect(int stripWidth, int stripHeight) { | ||
this.stripWidth = stripWidth; | ||
this.stripHeight = stripHeight; | ||
timestampsMs = new ArrayList<>(); | ||
} | ||
|
||
@Override | ||
public ThumbnailStripShaderProgram toGlShaderProgram(Context context, boolean useHdr) | ||
throws VideoFrameProcessingException { | ||
return new ThumbnailStripShaderProgram(context, useHdr, this); | ||
} | ||
|
||
/** | ||
* Sets the timestamps of the frames to draw, in milliseconds. | ||
* | ||
* <p>The timestamp represents the minimum presentation time of the next frame added to the strip. | ||
* For example, if the timestamp is 10, a frame with a time of 100 will be drawn but one with a | ||
* time of 9 will be ignored. | ||
*/ | ||
public void setTimestampsMs(List<Long> timestampsMs) { | ||
this.timestampsMs.clear(); | ||
this.timestampsMs.addAll(timestampsMs); | ||
currentThumbnailIndex = 0; | ||
} | ||
|
||
/** Returns whether all the thumbnails have already been drawn. */ | ||
public boolean isDone() { | ||
return currentThumbnailIndex >= timestampsMs.size(); | ||
} | ||
|
||
/** Returns the index of the next thumbnail to draw. */ | ||
public int getNextThumbnailIndex() { | ||
return currentThumbnailIndex; | ||
} | ||
|
||
/** Returns the timestamp in milliseconds of the next thumbnail to draw. */ | ||
public long getNextTimestampMs() { | ||
return isDone() ? C.TIME_END_OF_SOURCE : timestampsMs.get(currentThumbnailIndex); | ||
} | ||
|
||
/** Returns the total number of thumbnails to be drawn in the strip. */ | ||
public int getNumberOfThumbnails() { | ||
return timestampsMs.size(); | ||
} | ||
|
||
/* package */ void onThumbnailDrawn() { | ||
currentThumbnailIndex++; | ||
} | ||
} |
Oops, something went wrong.
e87f0d5
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#365
Is it possible to implement this function ?
e87f0d5
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@FongMi This commit is prototyping part of the functionality needed for generating a thumbnail strip, but there's still a lot of work to do to make it an easy to use and polished API. It will probably be a while until we have something that works out of the box.
e87f0d5
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks to reply, I see.