fix(ci): restore sanity checks

This commit is contained in:
edde746
2026-06-25 07:21:16 +02:00
parent 8affbb7af6
commit e90835c234
10 changed files with 54 additions and 35 deletions
@@ -39,14 +39,16 @@ internal class AssLatencyCalibrator(
private val overlaySurface: SurfaceView,
private val onCalibrated: (Int) -> Unit,
private val onDone: () -> Unit,
private val log: (String) -> Unit,
private val log: (String) -> Unit
) {
private val lock = Any()
private val finished = AtomicBoolean(false)
private val applyFail = AtomicInteger(0)
@Volatile private var frameIntervalNs: Long = 0L
@Volatile private var converged = false
@Volatile private var stopped = false
private var videoFrames = 0
@@ -77,7 +79,11 @@ internal class AssLatencyCalibrator(
doAttach = true
}
}
if (giveUp) finishIncomplete() else if (doAttach) attach(videoSurface, releaseTimeNs, SurfaceTxProbe.SOURCE_VIDEO)
if (giveUp) {
finishIncomplete()
} else if (doAttach) {
attach(videoSurface, releaseTimeNs, SurfaceTxProbe.SOURCE_VIDEO)
}
}
fun probeOverlay(releaseTimeNs: Long) {
@@ -114,7 +120,7 @@ internal class AssLatencyCalibrator(
surfaceCount: Int,
fenceState: Int,
source: Int,
callbackNs: Long,
callbackNs: Long
) {
if (surfaceCount <= 0 || fenceState != FENCE_OK || releaseNs <= 0) return
val relMs = (releaseNs - tag) / 1_000_000.0
@@ -168,6 +168,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
private var videoZoomScale: Float = 1.0f
private var assHandler: AssHandler? = null
private var assSubtitleView: AssSubtitleSurfaceView? = null
// Touched from the codec metadata listener, the GL-thread overlay hook, and the main-thread
// media-item-transition/dispose paths — keep visibility across threads.
@Volatile private var latencyCalibrator: AssLatencyCalibrator? = null
@@ -739,7 +740,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
overlaySurface = assView,
onCalibrated = { frames -> onAssLatencyCalibrated(frames) },
onDone = { assSubtitleView?.setPreSwapProbe(null) },
log = { msg -> emitLog("info", "ass-latency-cal", msg) },
log = { msg -> emitLog("info", "ass-latency-cal", msg) }
).also {
it.start()
// Bind the hook to THIS instance, not the volatile field, so a concurrent transition
@@ -36,7 +36,7 @@ object SurfaceTxProbe {
surfaceCount: Int,
fenceState: Int,
source: Int,
callbackNs: Long,
callbackNs: Long
) {
val s = sink ?: return
s.invoke(tag, latchNs, releaseNs, surfaceCount, fenceState, source, callbackNs)
+1 -1
View File
@@ -25,7 +25,7 @@ android {
// A shared cache keeps per-ABI CMake runs from redownloading libass.
arguments += listOf(
"-DANDROID_STL=c++_shared",
"-DLIBASS_CACHE_DIR=${layout.buildDirectory.get().asFile}/libass-prebuilt",
"-DLIBASS_CACHE_DIR=${layout.buildDirectory.get().asFile}/libass-prebuilt"
)
}
}
+4 -6
View File
@@ -298,8 +298,8 @@ JNIEXPORT jobject JNICALL Java_com_edde746_plezy_libass_AssRender_nativeAssRende
const jboolean hasOutput = imageListHasOutput(image) ? JNI_TRUE : JNI_FALSE;
if (tAss - t0 > 40) {
__android_log_print(
ANDROID_LOG_WARN, LOG_TAG, "slow render t=%lldms: ass=%lldms (changed=%d, hasOutput=%d)",
(long long)time, tAss - t0, changed, hasOutput == JNI_TRUE);
ANDROID_LOG_WARN, LOG_TAG, "slow render t=%lldms: ass=%lldms (changed=%d, hasOutput=%d)", (long long)time,
tAss - t0, changed, hasOutput == JNI_TRUE);
}
return (*env)->NewObject(env, atlasFrameClass, ctor, 0, 0, 0, changed, 0, hasOutput);
}
@@ -569,8 +569,7 @@ static EGLint gFtPresentName = EGL_DISPLAY_PRESENT_TIME_ANDROID;
// Probes the extension on the currently-current draw surface and enables capture.
// Re-resolves the display/surface each call so surface recreation is handled.
// Returns one of the FT_* codes above.
JNIEXPORT jint JNICALL
Java_com_edde746_plezy_libass_AssFrameTimestamps_nativeInit(JNIEnv* env, jclass clazz) {
JNIEXPORT jint JNICALL Java_com_edde746_plezy_libass_AssFrameTimestamps_nativeInit(JNIEnv* env, jclass clazz) {
gFtSurface = EGL_NO_SURFACE;
EGLDisplay dpy = eglGetCurrentDisplay();
EGLSurface surf = eglGetCurrentSurface(EGL_DRAW);
@@ -632,8 +631,7 @@ Java_com_edde746_plezy_libass_AssFrameTimestamps_nativeGetNextFrameId(JNIEnv* en
// Present (or composition) time for frameId (System.nanoTime() domain), or the
// PENDING(-2)/INVALID(-1) sentinels. Reported a few frames after the swap.
JNIEXPORT jlong JNICALL
Java_com_edde746_plezy_libass_AssFrameTimestamps_nativeGetDisplayPresentTime(
JNIEnv* env, jclass clazz, jlong frameId) {
Java_com_edde746_plezy_libass_AssFrameTimestamps_nativeGetDisplayPresentTime(JNIEnv* env, jclass clazz, jlong frameId) {
if (pEglGetFrameTimestamps == NULL || gFtSurface == EGL_NO_SURFACE) return EGL_TIMESTAMP_INVALID_ANDROID;
const EGLint names[1] = {gFtPresentName};
EGLnsecsANDROID values[1] = {0};
+7 -11
View File
@@ -67,8 +67,7 @@ static int64_t readFenceTimeNs(int fd, int* outStatus) {
if (ioctl(fd, SYNC_IOC_FILE_INFO, &probe) < 0) return -1;
*outStatus = probe.status;
if (probe.status != 1 || probe.num_fences == 0) return -1;
struct stp_fence_info* arr =
(struct stp_fence_info*)calloc(probe.num_fences, sizeof(struct stp_fence_info));
struct stp_fence_info* arr = (struct stp_fence_info*)calloc(probe.num_fences, sizeof(struct stp_fence_info));
if (!arr) return -1;
struct stp_file_info req;
memset(&req, 0, sizeof(req));
@@ -118,10 +117,7 @@ static int resolveSc(void) {
p_getControls = (pf_getControls)dlsym(h, "ASurfaceTransactionStats_getASurfaceControls");
p_prevRelease = (pf_prevRelease)dlsym(h, "ASurfaceTransactionStats_getPreviousReleaseFenceFd");
p_releaseControls = (pf_releaseControls)dlsym(h, "ASurfaceTransactionStats_releaseASurfaceControls");
gScOk = (p_fromJava && p_setOnComplete && p_latchTime && p_getControls && p_prevRelease &&
p_releaseControls)
? 1
: 0;
gScOk = (p_fromJava && p_setOnComplete && p_latchTime && p_getControls && p_prevRelease && p_releaseControls) ? 1 : 0;
return gScOk;
}
@@ -143,12 +139,12 @@ static int64_t nowMonoNs(void) {
return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec;
}
static void reportResult(JNIEnv* env, int64_t tag, int64_t latchNs, int64_t releaseNs, int count,
int fenceState, int source, int64_t cbNs) {
static void reportResult(
JNIEnv* env, int64_t tag, int64_t latchNs, int64_t releaseNs, int count, int fenceState, int source, int64_t cbNs) {
if (!env || !gProbeClass || !gOnResult) return;
(*env)->CallStaticVoidMethod(env, gProbeClass, gOnResult, (jlong)tag, (jlong)latchNs,
(jlong)releaseNs, (jint)count, (jint)fenceState, (jint)source,
(jlong)cbNs);
(*env)->CallStaticVoidMethod(
env, gProbeClass, gOnResult, (jlong)tag, (jlong)latchNs, (jlong)releaseNs, (jint)count, (jint)fenceState,
(jint)source, (jlong)cbNs);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
}
@@ -213,10 +213,9 @@ internal class AssAtlasPipeline(
// first actual render. Confined to the libass thread after creation.
private var slots: AtlasSlots? = null
private fun rendererStateGeneration(): Long =
assHandler.render?.let {
(System.identityHashCode(it).toLong() shl 32) or (it.stateGeneration.toLong() and 0xffffffffL)
} ?: -1L
private fun rendererStateGeneration(): Long = assHandler.render?.let {
(System.identityHashCode(it).toLong() shl 32) or (it.stateGeneration.toLong() and 0xffffffffL)
} ?: -1L
private fun acquireSlots(): AtlasSlots {
slots?.let { return it }
@@ -484,8 +483,10 @@ private class SubtitleFramePhaseEstimator {
val ptsDeltaUs = ptsUs - prevPtsUs
val releaseDeltaNs = releaseNs - prevReleaseNs
if (ptsDeltaUs <= 0 || ptsDeltaUs > MAX_DELTA_US ||
releaseDeltaNs <= 0 || releaseDeltaNs > MAX_DELTA_NS
if (ptsDeltaUs <= 0 ||
ptsDeltaUs > MAX_DELTA_US ||
releaseDeltaNs <= 0 ||
releaseDeltaNs > MAX_DELTA_NS
) {
deltaCount = 0
deltaIndex = 0
@@ -504,8 +505,7 @@ private class SubtitleFramePhaseEstimator {
return copy[count / 2]
}
private fun saturatedAdd(value: Long, delta: Long): Long =
if (delta > 0 && value > Long.MAX_VALUE - delta) Long.MAX_VALUE else value + delta
private fun saturatedAdd(value: Long, delta: Long): Long = if (delta > 0 && value > Long.MAX_VALUE - delta) Long.MAX_VALUE else value + delta
private companion object {
const val UNSET = Long.MIN_VALUE
@@ -947,12 +947,12 @@ private class AtlasGlThread(
copy.sort()
measuredIntervalNs = copy[releaseDeltaCount / 2]
swapLeadNs = (measuredIntervalNs / 2).coerceIn(
SCHEDULED_SWAP_LEAD_MIN_NS, SCHEDULED_SWAP_LEAD_MAX_NS
SCHEDULED_SWAP_LEAD_MIN_NS,
SCHEDULED_SWAP_LEAD_MAX_NS
)
}
}
private lateinit var handler: Handler
private var eglDisplay: EGLDisplay = EGL14.EGL_NO_DISPLAY
private var eglContext: EGLContext = EGL14.EGL_NO_CONTEXT
@@ -1322,6 +1322,7 @@ private class AtlasGlThread(
private const val MSG_SIZE_CHANGED = 3
private const val MSG_RELEASE = 4
private const val SYNC_LOG_INTERVAL_SWAPS = 120L
// Missing SurfaceFlinger's latch deadline costs a full refresh, so keep the
// margin proportional to the active cadence.
private const val SCHEDULED_SWAP_LEAD_MIN_NS = 6_000_000L
@@ -210,8 +210,7 @@ internal class SpecRenderEngine(
return SpecWrite(target, frame)
}
private fun isImplicitBlank(frame: AssAtlasFrame): Boolean =
!frame.hasOutput && libassLastFrame?.hasOutput == true
private fun isImplicitBlank(frame: AssAtlasFrame): Boolean = !frame.hasOutput && libassLastFrame?.hasOutput == true
/**
* Pre-renders [ptsUs] (an upcoming event's start) purely to warm the
+9
View File
@@ -7,10 +7,19 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/mpv/mpv.dart';
import 'package:plezy/mpv/player/platform/player_android.dart';
import 'package:plezy/mpv/player/player_native.dart';
import 'package:plezy/services/settings_service.dart';
import '../test_helpers/prefs.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() async {
resetSharedPreferencesForTest();
SettingsService.resetForTesting();
await SettingsService.getInstance();
});
group('player open', () {
test('ExoPlayer clears stale Dart track state before opening new media', () async {
await _withMockChannels(
+9
View File
@@ -3,6 +3,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/mpv/player/platform/player_android.dart';
import 'package:plezy/mpv/player/player_base.dart';
import 'package:plezy/mpv/player/player_native.dart';
import 'package:plezy/services/settings_service.dart';
import '../test_helpers/prefs.dart';
/// Guards the channel contract: every property [PlayerBase.handlePropertyChange]
/// depends on for core state must be registered by each backend at init.
@@ -12,6 +15,12 @@ import 'package:plezy/mpv/player/player_native.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() async {
resetSharedPreferencesForTest();
SettingsService.resetForTesting();
await SettingsService.getInstance();
});
const coreNames = {
'time-pos',
'duration',