Files
utun2/tools/chatgui-android/tests/stream_worker_smoke/StreamWorkerSmoke.java
T

133 lines
8.2 KiB
Java

import android.media.ImageReader;
import android.graphics.ImageFormat;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.view.Surface;
import java.lang.reflect.*;
import java.nio.file.*;
import java.util.concurrent.atomic.*;
/** Device smoke test: uses production classes from APK, without installing/stopping the user's app. */
public class StreamWorkerSmoke {
static Object invoke(Object owner, String name, Object... args) throws Exception {
for (Method m : owner.getClass().getDeclaredMethods()) {
if (m.getName().equals(name) && m.getParameterCount() == args.length) {
m.setAccessible(true);
try { return m.invoke(owner, args); }
catch (InvocationTargetException e) { throw new RuntimeException(name, e.getCause()); }
}
}
throw new NoSuchMethodException(name);
}
static Class<?> type(String name) throws Exception { return Class.forName("com.utun.chat.data." + name); }
static Object callback(Runnable body) throws Exception {
Class<?> function = Class.forName("kotlin.jvm.functions.Function0");
Object unit = Class.forName("kotlin.Unit").getField("INSTANCE").get(null);
return Proxy.newProxyInstance(function.getClassLoader(), new Class<?>[]{function}, (p, m, a) -> {
if (m.getName().equals("invoke")) { body.run(); return unit; }
if (m.getName().equals("toString")) return "smoke callback";
return null;
});
}
static long underruns(String text) {
return Long.parseLong(text.split("underruns=")[1].split(" ")[0]);
}
static long played(String text) { return Long.parseLong(text.split("played=")[1].split(" ")[0]); }
public static void main(String[] args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> { e.printStackTrace(); System.exit(1); });
if (Looper.getMainLooper() == null) Looper.prepareMainLooper();
HandlerThread images = new HandlerThread("smoke-images"); images.start();
ImageReader reader = ImageReader.newInstance(128, 72, ImageFormat.PRIVATE, 4);
AtomicInteger received = new AtomicInteger();
reader.setOnImageAvailableListener(r -> {
try (android.media.Image image = r.acquireLatestImage()) { if (image != null) received.incrementAndGet(); }
}, new Handler(images.getLooper()));
Object noop = callback(() -> {});
Object nativeLib = type("NativeLib").getField("INSTANCE").get(null);
long opus = (Long)invoke(nativeLib, "streamOpusCreate", true);
byte[] packet = (byte[])invoke(nativeLib, "nativeStreamOpusEncode", opus, new short[1920], false, 25, 1);
invoke(nativeLib, "streamOpusClose", opus);
if (packet == null) throw new AssertionError("Opus encode failed");
byte[] frame = Files.readAllBytes(Paths.get(args[0]));
Object codec = type("StreamVideoCodec").getField("H264").get(null);
Class<?> function = Class.forName("kotlin.jvm.functions.Function0");
Constructor<?> audioCtor = type("StreamMixedPlayback").getConstructor(long.class, long.class, boolean.class, function);
Constructor<?> frameCtor = type("StreamVideoPlayback$Frame").getConstructor(long.class, boolean.class,
int.class, int.class, int.class, long.class, type("StreamVideoCodec"), byte[].class, byte[].class);
AtomicReference<Object> currentAudio = new AtomicReference<>();
AtomicInteger shown = new AtomicInteger();
AtomicBoolean stalled = new AtomicBoolean();
AtomicReference<Throwable> problem = new AtomicReference<>();
Object render = callback(() -> {
if (shown.incrementAndGet() == 15) {
try {
Object audio = currentAudio.get();
String before = (String)invoke(audio, "getDescription");
System.out.println("STALL start " + before);
Thread.sleep(500);
String after = (String)invoke(audio, "getDescription");
System.out.println("STALL end " + after);
if (underruns(after) != underruns(before)) throw new AssertionError("Audio gap during video stall");
if (played(after) - played(before) < 20000) throw new AssertionError("Audio clock stalled with video");
stalled.set(true);
} catch (Throwable e) { problem.set(e); }
}
});
Object video = type("StreamVideoPlayback").getConstructor(long.class, Surface.class, long.class, function, function)
.newInstance(4242L, reader.getSurface(), System.nanoTime() / 1000, noop, render);
try {
for (int phase = 0; phase < 2; phase++) {
if (phase != 0) invoke(video, "reset");
int frames = phase == 0 ? 120 : 3;
long duration = frames * 33333L;
Object audio = audioCtor.newInstance(4242L + phase, 0L, true, noop);
currentAudio.set(audio);
long audioPts = 0, position = 0, deadline = System.nanoTime() + 15_000_000_000L;
int supplied = 0;
boolean videoFinished = false, audioFinished = false, done = false;
try {
while (System.nanoTime() < deadline) {
Object failure = invoke(video, "getFailure");
if (failure != null) throw new AssertionError("Video failed", (Throwable)failure);
if (problem.get() != null) throw new AssertionError("Stall test failed", problem.get());
while (audioPts < duration && (Boolean)invoke(audio, "getNeedsInput")) {
int result = (Integer)invoke(audio, "feed", audioPts, packet);
if (result < 0) throw new AssertionError("Opus feed=" + result);
audioPts += 20000;
}
if (audioPts >= duration && !audioFinished) { invoke(audio, "finish"); audioFinished = true; }
invoke(audio, "prepare");
while (supplied < frames && (Boolean)invoke(video, "getWantsInput")) {
invoke(video, "offer", frameCtor.newInstance(supplied++ * 33333L, true, 128, 72, 0, 1L,
codec, new byte[0], frame));
}
if (supplied == frames && !videoFinished) { invoke(video, "finish"); videoFinished = true; }
Object state = invoke(video, "getState");
long lead = (Long)invoke(audio, "getLeadUs");
if (invoke(state, "getFirstOutputPts") != null) {
long[] clock = (long[])invoke(audio, "update", System.nanoTime() / 1000, audioPts, Math.max(40000L, lead + 20000));
if (clock == null) throw new AssertionError("Clock failed");
position = clock[0];
}
invoke(video, "update", position, audioPts, lead);
if ((Boolean)invoke(state, "getEnded") && (Boolean)invoke(audio, "getDrained")) {
long decoded = (Long)invoke(state, "getDecoded");
if (decoded != frames) throw new AssertionError("Lost frames " + state);
System.out.println("PASS phase=" + phase + " " + state + " " + invoke(audio, "getDescription"));
done = true; break;
}
Thread.sleep(5);
}
if (!done) throw new AssertionError("Drain timeout " + invoke(video, "getState") + " " + invoke(audio, "getDescription"));
} finally { invoke(audio, "close"); }
}
if (!stalled.get()) throw new AssertionError("Stall was not exercised");
System.out.println("PASS 500ms video stall: no new audio gaps; EOF/reset: 120 then 3 decoded; surfaces=" + received.get());
} finally {
invoke(video, "close"); reader.close(); images.quitSafely(); images.join();
}
System.exit(0);
}
}