soft sync audio via PlaybackParameters speed (rate-based, no seekTo jump)
Master = glasses (5s PlaybackPosition), slave = phone audio. Replace aggressive seekTo-drift-corrector with smooth tempo correction: - |diff|<=150ms (TRIGGER_BAND) -> speed 1f (hysteresis, no flutter) - audio lags -> speed 1.02 (catch up) - audio ahead -> speed 0.98 (slow down) - |diff|>2000ms -> rare hard seekTo + reset speed 1f - removed catch-up seekTo after +1s (was adding ~2s error) - log now prints speed AudioSyncPlayerTest: 18 cases (boundaries 150/2000, speed up/down/1f, play/pause).
This commit is contained in:
@@ -12,58 +12,98 @@ import kotlin.test.assertTrue
|
||||
|
||||
class AudioSyncPlayerTest {
|
||||
|
||||
@Test
|
||||
fun noSeekWhenWithinThreshold() {
|
||||
val d = decideSync(audioPosition = 10_000, videoPosition = 10_800, audioPlaying = true, videoPlaying = true)
|
||||
assertNull(d.seekToMs, "diff=800мс ≤ 1000мс — seek не нужен")
|
||||
assertNull(d.playing, "play/pause совпадают — не трогаем")
|
||||
}
|
||||
// --- мягкая синхронизация через скорость (decideSync): speed вместо резкого seek ---
|
||||
|
||||
@Test
|
||||
fun thresholdBoundaryDiff1000DoesNotSeek() {
|
||||
val d = decideSync(audioPosition = 10_000, videoPosition = 11_000, audioPlaying = true, videoPlaying = true)
|
||||
assertNull(d.seekToMs, "diff=1000мс не больше порога — не дёргаем")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun seeksToVideoPositionWhenDiffAboveThreshold() {
|
||||
val d = decideSync(audioPosition = 10_000, videoPosition = 12_000, audioPlaying = true, videoPlaying = true)
|
||||
assertEquals(12_000L, d.seekToMs, "diff=2000мс > 1000мс — перематываем на позицию очков")
|
||||
fun noCorrectionWhenFullyAligned() {
|
||||
val d = decideSync(audioPosition = 42_000, videoPosition = 42_000, audioPlaying = true, videoPlaying = true)
|
||||
assertEquals(1f, d.speed, "полное совпадение — темп не трогаем")
|
||||
assertNull(d.seekToMs)
|
||||
assertNull(d.playing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun seeksWhenAudioAheadOfVideo() {
|
||||
fun speedStaysFlatInsideTriggerBand() {
|
||||
val d = decideSync(audioPosition = 10_000, videoPosition = 10_100, audioPlaying = true, videoPlaying = true)
|
||||
assertEquals(1f, d.speed, "diff=100мс внутри гистерезиса [-150;+150] — дребезга нет")
|
||||
assertNull(d.seekToMs)
|
||||
assertNull(d.playing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun speedsUpWhenAudioLagsBehind() {
|
||||
val d = decideSync(audioPosition = 10_000, videoPosition = 10_300, audioPlaying = true, videoPlaying = true)
|
||||
assertEquals(1f + DRIFT_RATE, d.speed, "звук отстаёт — догоняем ускорением ${1f + DRIFT_RATE}")
|
||||
assertNull(d.seekToMs, "diff=300мс внутри HARD-порога — seek не нужен")
|
||||
assertNull(d.playing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun slowsDownWhenAudioAhead() {
|
||||
val d = decideSync(audioPosition = 10_300, videoPosition = 10_000, audioPlaying = true, videoPlaying = true)
|
||||
assertEquals(1f - DRIFT_RATE, d.speed, "звук впереди — тормозим до ${1f - DRIFT_RATE}")
|
||||
assertNull(d.seekToMs)
|
||||
assertNull(d.playing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bandBoundaryExactly150MsStaysFlat() {
|
||||
val ahead = decideSync(audioPosition = 10_150, videoPosition = 10_000, audioPlaying = true, videoPlaying = true)
|
||||
assertEquals(1f, ahead.speed, "diff=+150мс — граница закрытой зоны, темп 1f")
|
||||
val behind = decideSync(audioPosition = 9_850, videoPosition = 10_000, audioPlaying = true, videoPlaying = true)
|
||||
assertEquals(1f, behind.speed, "diff=-150мс — граница закрытой зоны, темп 1f")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hardSeekWhenAudioFarBehind() {
|
||||
val d = decideSync(audioPosition = 10_000, videoPosition = 12_500, audioPlaying = true, videoPlaying = true)
|
||||
assertEquals(12_500L, d.seekToMs, "diff=2500мс > 2000мс — жёсткий seek на позицию очков")
|
||||
assertEquals(1f, d.speed, "после seek темп возвращается в 1f — дальше мягко")
|
||||
assertNull(d.playing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hardSeekWhenAudioFarAhead() {
|
||||
val d = decideSync(audioPosition = 25_000, videoPosition = 20_000, audioPlaying = true, videoPlaying = true)
|
||||
assertEquals(20_000L, d.seekToMs)
|
||||
assertEquals(1f, d.speed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hardThresholdBoundaryDoesNotSeek() {
|
||||
val d = decideSync(audioPosition = 10_000, videoPosition = 12_000, audioPlaying = true, videoPlaying = true)
|
||||
assertNull(d.seekToMs, "diff=2000мс не БОЛЬШЕ порога — подгоняем скоростью, без рывка")
|
||||
assertEquals(1f + DRIFT_RATE, d.speed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hardThresholdPlusOneSeeks() {
|
||||
val d = decideSync(audioPosition = 10_000, videoPosition = 12_001, audioPlaying = true, videoPlaying = true)
|
||||
assertEquals(12_001L, d.seekToMs, "diff=2001мс > 2000мс — уже жёсткий seek")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playWhenVideoPlayingButAudioPaused() {
|
||||
val d = decideSync(audioPosition = 10_000, videoPosition = 10_000, audioPlaying = false, videoPlaying = true)
|
||||
assertNull(d.seekToMs)
|
||||
assertEquals(1f, d.speed, "play/pause влияние только на playing, не на скорость")
|
||||
assertEquals(true, d.playing, "очки играют — аудио должно играть")
|
||||
assertNull(d.seekToMs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pauseWhenVideoPausedButAudioPlaying() {
|
||||
val d = decideSync(audioPosition = 10_000, videoPosition = 10_000, audioPlaying = true, videoPlaying = false)
|
||||
assertNull(d.seekToMs)
|
||||
assertEquals(1f, d.speed)
|
||||
assertEquals(false, d.playing, "очки на паузе — аудио должно паузиться")
|
||||
assertNull(d.seekToMs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun seekAndPlayApplyTogether() {
|
||||
val d = decideSync(audioPosition = 5_000, videoPosition = 8_000, audioPlaying = false, videoPlaying = true)
|
||||
assertEquals(8_000L, d.seekToMs)
|
||||
val d = decideSync(audioPosition = 5_000, videoPosition = 15_000, audioPlaying = false, videoPlaying = true)
|
||||
assertEquals(15_000L, d.seekToMs)
|
||||
assertEquals(true, d.playing, "и seek, и play срабатывают в одном sync")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noCorrectionWhenFullyAligned() {
|
||||
val d = decideSync(audioPosition = 42_000, videoPosition = 42_000, audioPlaying = true, videoPlaying = true)
|
||||
assertNull(d.seekToMs)
|
||||
assertNull(d.playing)
|
||||
assertEquals(1f, d.speed)
|
||||
}
|
||||
|
||||
// --- resolveAudioSourceDetailed (чистая функция, ветки причины «нет звука») ---
|
||||
|
||||
Reference in New Issue
Block a user