Skip to content

Commit 5586c5c

Browse files
ychaparovcopybara-github
authored andcommitted
Add ExoPlayerFrameDropAnalysisTest test
Add an analysis test to evaluate ExoPlayer frame dropping algorithms. Uses a custom Clock that skips forward to force dropped frames PiperOrigin-RevId: 761487984
1 parent 5d5a430 commit 5586c5c

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package androidx.media3.transformer.mh.analysis;
17+
18+
import static androidx.media3.common.util.Assertions.checkState;
19+
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
20+
import static com.google.common.collect.ImmutableList.toImmutableList;
21+
22+
import android.content.Context;
23+
import android.net.Uri;
24+
import android.view.SurfaceView;
25+
import androidx.media3.common.C;
26+
import androidx.media3.common.MediaItem;
27+
import androidx.media3.common.TrackSelectionParameters;
28+
import androidx.media3.common.util.SystemClock;
29+
import androidx.media3.exoplayer.DecoderCounters;
30+
import androidx.media3.exoplayer.DefaultRenderersFactory;
31+
import androidx.media3.exoplayer.ExoPlayer;
32+
import androidx.media3.exoplayer.analytics.AnalyticsListener;
33+
import androidx.media3.transformer.AndroidTestUtil;
34+
import androidx.media3.transformer.PlayerTestListener;
35+
import androidx.media3.transformer.SurfaceTestActivity;
36+
import androidx.test.core.app.ApplicationProvider;
37+
import androidx.test.ext.junit.rules.ActivityScenarioRule;
38+
import com.google.common.collect.ImmutableSet;
39+
import com.google.common.collect.Sets;
40+
import java.util.List;
41+
import java.util.concurrent.atomic.AtomicReference;
42+
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
43+
import org.json.JSONException;
44+
import org.json.JSONObject;
45+
import org.junit.After;
46+
import org.junit.Before;
47+
import org.junit.Ignore;
48+
import org.junit.Rule;
49+
import org.junit.Test;
50+
import org.junit.rules.TestName;
51+
import org.junit.runner.RunWith;
52+
import org.junit.runners.Parameterized;
53+
import org.junit.runners.Parameterized.Parameter;
54+
import org.junit.runners.Parameterized.Parameters;
55+
56+
/** Instrumentation tests for analyzing {@link ExoPlayer} frame dropping behavior. */
57+
@RunWith(Parameterized.class)
58+
@Ignore("Analysis tests do not verify correctness take a long time to run.")
59+
public class ExoPlayerFrameDropAnalysisTest {
60+
private static final ImmutableSet<String> INPUT_ASSETS =
61+
ImmutableSet.of(
62+
"asset:///media/mp4/long_4k_av1.mp4",
63+
"asset:///media/mp4/long_1080p_av1.mp4",
64+
"asset:///media/mp4/long_180p_av1.mp4");
65+
66+
private static final long TIMEOUT_MS = 120_000;
67+
private static final long TEST_PLAYBACK_DURATION_MS = 30_000;
68+
private static final long CLOCK_JUMP_INTERVAL_MS = 2_000;
69+
private static final long CLOCK_JUMP_AMOUNT_MS = 300;
70+
71+
@Rule public final TestName testName = new TestName();
72+
73+
@Rule
74+
public ActivityScenarioRule<SurfaceTestActivity> rule =
75+
new ActivityScenarioRule<>(SurfaceTestActivity.class);
76+
77+
@Parameter(0)
78+
public TestConfig testConfig;
79+
80+
@Parameters(name = "{0}")
81+
public static List<TestConfig> parameters() {
82+
return Sets.cartesianProduct(
83+
INPUT_ASSETS,
84+
ImmutableSet.of(0.4f, 0.5f, 1f, 2f),
85+
ImmutableSet.of(0L, 15_000L, 50_000L))
86+
.stream()
87+
.map(
88+
testConfigArguments ->
89+
new TestConfig(
90+
/* uri= */ (String) testConfigArguments.get(0),
91+
/* playbackSpeed= */ (Float) testConfigArguments.get(1),
92+
/* lateThresholdUs= */ (Long) testConfigArguments.get(2)))
93+
.collect(toImmutableList());
94+
}
95+
96+
private final Context context = ApplicationProvider.getApplicationContext();
97+
private @MonotonicNonNull ExoPlayer player;
98+
private SurfaceView surfaceView;
99+
100+
@Before
101+
public void setUp() {
102+
rule.getScenario().onActivity(activity -> surfaceView = activity.getSurfaceView());
103+
}
104+
105+
@After
106+
public void tearDown() {
107+
getInstrumentation()
108+
.runOnMainSync(
109+
() -> {
110+
if (player != null) {
111+
player.release();
112+
}
113+
});
114+
}
115+
116+
@Test
117+
public void analyzeExoPlayerFrameDrops() throws Exception {
118+
PlayerTestListener playerTestListener = new PlayerTestListener(TIMEOUT_MS);
119+
MediaItem mediaItem =
120+
new MediaItem.Builder()
121+
.setUri(testConfig.uri)
122+
.setClippingConfiguration(
123+
new MediaItem.ClippingConfiguration.Builder()
124+
.setEndPositionMs((long) (TEST_PLAYBACK_DURATION_MS * testConfig.playbackSpeed))
125+
.build())
126+
.build();
127+
AtomicReference<String> decoderName = new AtomicReference<>();
128+
AtomicReference<DecoderCounters> decoderCounters = new AtomicReference<>();
129+
AnalyticsListener videoDecoderListener =
130+
new AnalyticsListener() {
131+
@Override
132+
public void onVideoDecoderInitialized(
133+
EventTime eventTime,
134+
String newDecoderName,
135+
long initializedTimestampMs,
136+
long initializationDurationMs) {
137+
checkState(decoderName.compareAndSet(/* expectedValue= */ null, newDecoderName));
138+
}
139+
140+
@Override
141+
public void onVideoEnabled(EventTime eventTime, DecoderCounters newDecoderCounters) {
142+
checkState(
143+
decoderCounters.compareAndSet(/* expectedValue= */ null, newDecoderCounters));
144+
}
145+
};
146+
getInstrumentation()
147+
.runOnMainSync(
148+
() -> {
149+
player =
150+
new ExoPlayer.Builder(
151+
context,
152+
new DefaultRenderersFactory(context)
153+
.experimentalSetParseAv1SampleDependencies(
154+
/* parseAv1SampleDependencies= */ testConfig.lateThresholdUs != 0)
155+
.experimentalSetLateThresholdToDropDecoderInputUs(
156+
testConfig.lateThresholdUs))
157+
.setClock(new JumpingClock())
158+
.build();
159+
player.setTrackSelectionParameters(
160+
new TrackSelectionParameters.Builder()
161+
.setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_AUDIO))
162+
.build());
163+
player.setMediaItem(mediaItem);
164+
player.setPlaybackSpeed(testConfig.playbackSpeed);
165+
player.setVideoSurfaceView(surfaceView);
166+
player.addListener(playerTestListener);
167+
player.addAnalyticsListener(videoDecoderListener);
168+
player.prepare();
169+
player.setPlayWhenReady(true);
170+
});
171+
172+
playerTestListener.waitUntilPlayerEnded();
173+
174+
JSONObject resultJson = testConfig.toJsonObject();
175+
resultJson.put("decoderName", decoderName.get());
176+
resultJson.put("queuedInputBufferCount", decoderCounters.get().queuedInputBufferCount);
177+
resultJson.put("droppedBufferCount", decoderCounters.get().droppedBufferCount);
178+
resultJson.put("droppedInputBufferCount", decoderCounters.get().droppedInputBufferCount);
179+
resultJson.put(
180+
"maxConsecutiveDroppedBufferCount", decoderCounters.get().maxConsecutiveDroppedBufferCount);
181+
resultJson.put("droppedToKeyframeCount", decoderCounters.get().droppedToKeyframeCount);
182+
AndroidTestUtil.writeTestSummaryToFile(
183+
ApplicationProvider.getApplicationContext(),
184+
/* testId= */ testName.getMethodName(),
185+
resultJson);
186+
}
187+
188+
private static class TestConfig {
189+
public final String uri;
190+
public final float playbackSpeed;
191+
public final long lateThresholdUs;
192+
193+
public TestConfig(String uri, float playbackSpeed, long lateThresholdUs) {
194+
this.uri = uri;
195+
this.playbackSpeed = playbackSpeed;
196+
this.lateThresholdUs = lateThresholdUs;
197+
}
198+
199+
@Override
200+
public String toString() {
201+
return String.format(
202+
"%s_sp_%f_lateUs_%d",
203+
Uri.parse(uri).getLastPathSegment(), playbackSpeed, lateThresholdUs);
204+
}
205+
206+
public JSONObject toJsonObject() throws JSONException {
207+
JSONObject resultJson = new JSONObject();
208+
resultJson.put("file", Uri.parse(uri).getLastPathSegment());
209+
resultJson.put("playbackSpeed", playbackSpeed);
210+
resultJson.put("lateThresholdUs", lateThresholdUs);
211+
return resultJson;
212+
}
213+
}
214+
215+
private static class JumpingClock extends SystemClock {
216+
private long offsetMs;
217+
private long nextJumpMs;
218+
219+
public JumpingClock() {
220+
super();
221+
nextJumpMs = super.elapsedRealtime() + CLOCK_JUMP_INTERVAL_MS;
222+
}
223+
224+
@Override
225+
public long elapsedRealtime() {
226+
long clockTimeMs = super.elapsedRealtime();
227+
if (clockTimeMs >= nextJumpMs) {
228+
nextJumpMs += CLOCK_JUMP_INTERVAL_MS;
229+
offsetMs += CLOCK_JUMP_AMOUNT_MS;
230+
}
231+
232+
return clockTimeMs + offsetMs;
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)