Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95d1a7b9ec |
+60
-7
@@ -195,6 +195,59 @@ const state = {
|
|||||||
degradedAt: 0
|
degradedAt: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FOLLOW THE BOTTOM ONLY WHILE THE OPERATOR IS AT THE BOTTOM.
|
||||||
|
*
|
||||||
|
* Every one of these boxes used to end its update with
|
||||||
|
* `box.scrollTop = box.scrollHeight`, unconditionally. While the service is
|
||||||
|
* running that fires on every sentence, so scrolling back to re-read what was
|
||||||
|
* just said was impossible: the box yanked itself to the live edge a fraction
|
||||||
|
* of a second later, every time. Reported from the desk as "can't scroll on
|
||||||
|
* what's being said when it's live", which is exactly what it was.
|
||||||
|
*
|
||||||
|
* A box follows the live edge until the operator scrolls away from it, and
|
||||||
|
* starts following again when they come back to it. Nothing has to be pressed
|
||||||
|
* to get out, and nothing has to be pressed to get back in.
|
||||||
|
*
|
||||||
|
* The slack matters. A programmatic jump to the bottom also raises a scroll
|
||||||
|
* event, and it lands AT the bottom, so sticking survives its own scrolling.
|
||||||
|
* Sub-pixel layout and a growing last line can leave a couple of pixels
|
||||||
|
* behind, which with a zero threshold would unstick the box on its own output.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* Trimming the TOP of a box the operator is reading moves the text under
|
||||||
|
* their eyes — the other half of "it keeps jumping". While they are scrolled
|
||||||
|
* away the cap is suspended, so the lines they are looking at stay put; when
|
||||||
|
* they return to the live edge the backlog is trimmed in one go. The ceiling
|
||||||
|
* keeps a box that is left scrolled up all service from growing without end.
|
||||||
|
*/
|
||||||
|
function trimTop(box, cap) {
|
||||||
|
if (!box) return;
|
||||||
|
const held = box.dataset.stick === '0';
|
||||||
|
const limit = held ? cap * 6 : cap;
|
||||||
|
while (box.children.length > limit) box.removeChild(box.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const STICK_SLACK_PX = 48;
|
||||||
|
const stickWired = new WeakSet();
|
||||||
|
|
||||||
|
function followBottom(box) {
|
||||||
|
if (!box) return;
|
||||||
|
if (!stickWired.has(box)) {
|
||||||
|
stickWired.add(box);
|
||||||
|
box.dataset.stick = '1';
|
||||||
|
box.addEventListener('scroll', () => {
|
||||||
|
const atBottom =
|
||||||
|
box.scrollHeight - box.scrollTop - box.clientHeight <= STICK_SLACK_PX;
|
||||||
|
box.dataset.stick = atBottom ? '1' : '0';
|
||||||
|
}, { passive: true });
|
||||||
|
}
|
||||||
|
/* Scrolled away: leave the view exactly where they put it. New content
|
||||||
|
grows BELOW the viewport, so not touching scrollTop is what holds the
|
||||||
|
reader's place — there is nothing else to do here. */
|
||||||
|
if (box.dataset.stick !== '0') box.scrollTop = box.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────── logging
|
// ────────────────────────────────────────────────────────── logging
|
||||||
|
|
||||||
function log(msg, kind) {
|
function log(msg, kind) {
|
||||||
@@ -208,8 +261,8 @@ function log(msg, kind) {
|
|||||||
if (kind) line.className = kind;
|
if (kind) line.className = kind;
|
||||||
line.textContent = new Date().toLocaleTimeString() + ' ' + msg;
|
line.textContent = new Date().toLocaleTimeString() + ' ' + msg;
|
||||||
el.appendChild(line);
|
el.appendChild(line);
|
||||||
while (el.children.length > 200) el.removeChild(el.firstChild);
|
trimTop(el, 200);
|
||||||
el.scrollTop = el.scrollHeight;
|
followBottom(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hero(stateName, title, body) {
|
function hero(stateName, title, body) {
|
||||||
@@ -2471,8 +2524,8 @@ function monitorFinal(cid, text) {
|
|||||||
monRows.set(cid, el);
|
monRows.set(cid, el);
|
||||||
/* Neither the map nor the box may grow for the length of a service. */
|
/* Neither the map nor the box may grow for the length of a service. */
|
||||||
while (monRows.size > 60) monRows.delete(monRows.keys().next().value);
|
while (monRows.size > 60) monRows.delete(monRows.keys().next().value);
|
||||||
while (box.children.length > 24) box.removeChild(box.firstChild);
|
trimTop(box, 24);
|
||||||
box.scrollTop = box.scrollHeight;
|
followBottom(box);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -2514,7 +2567,7 @@ function monitorTranslation(cid, lang, text) {
|
|||||||
row.classList.add('dropped');
|
row.classList.add('dropped');
|
||||||
}
|
}
|
||||||
const box = $('monitor');
|
const box = $('monitor');
|
||||||
if (box) box.scrollTop = box.scrollHeight;
|
followBottom(box);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -2581,9 +2634,9 @@ function showMonitor(text, isPartial, langCount) {
|
|||||||
sub.textContent = langCount + ' language' + (langCount === 1 ? '' : 's') + ' · ' + state.lagMs + ' ms';
|
sub.textContent = langCount + ' language' + (langCount === 1 ? '' : 's') + ' · ' + state.lagMs + ' ms';
|
||||||
el.appendChild(sub);
|
el.appendChild(sub);
|
||||||
box.appendChild(el);
|
box.appendChild(el);
|
||||||
while (box.children.length > 30) box.removeChild(box.firstChild);
|
trimTop(box, 30);
|
||||||
}
|
}
|
||||||
box.scrollTop = box.scrollHeight;
|
followBottom(box);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────── church server
|
// ────────────────────────────────────────────────────────── church server
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
"name": "church-translate-engine",
|
"name": "church-translate-engine",
|
||||||
"productName": "Church Translate",
|
"productName": "Church Translate",
|
||||||
"version": "3.23.2",
|
"version": "3.23.2",
|
||||||
"buildNumber": 125,
|
"buildNumber": 126,
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Live sermon translation — self-contained: captures audio, transcribes, translates, and serves the congregation page itself",
|
"description": "Live sermon translation — self-contained: captures audio, transcribes, translates, and serves the congregation page itself",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"asar": false,
|
"asar": false,
|
||||||
"buildVersion": "125",
|
"buildVersion": "126",
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -519,7 +519,16 @@
|
|||||||
/* After any interruption the player resumes where it left off and stays
|
/* After any interruption the player resumes where it left off and stays
|
||||||
behind live forever — no error, no stall, just permanently late. Every
|
behind live forever — no error, no stall, just permanently late. Every
|
||||||
one of these events is a chance to catch back up. */
|
one of these events is a chance to catch back up. */
|
||||||
['playing', 'stalled', 'waiting', 'canplay', 'loadedmetadata'].forEach(function (ev) {
|
/*
|
||||||
|
* 'stalled' and 'waiting' are deliberately NOT in this list. A seek on a
|
||||||
|
* live HLS stream makes the element re-buffer, which raises exactly those
|
||||||
|
* two events — so seeking from them is a loop that feeds itself: seek,
|
||||||
|
* stall, seek, stall. On Safari, where HLS is played natively and a seek
|
||||||
|
* lands on a segment boundary, each turn of that loop replays or skips
|
||||||
|
* part of a sentence. That is the "it repeats a bit two or three times
|
||||||
|
* then moves on" reported from iPhones.
|
||||||
|
*/
|
||||||
|
['playing', 'canplay', 'loadedmetadata'].forEach(function (ev) {
|
||||||
a.addEventListener(ev, function () { seekLive(); });
|
a.addEventListener(ev, function () { seekLive(); });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -599,8 +608,28 @@
|
|||||||
* and 4s of tolerance plus a 5s check interval meant real drift could reach
|
* and 4s of tolerance plus a 5s check interval meant real drift could reach
|
||||||
* nine seconds before anyone looked.
|
* nine seconds before anyone looked.
|
||||||
*/
|
*/
|
||||||
var DRIFT_MAX = 1.5;
|
/*
|
||||||
var DRIFT_LAND = 0.6;
|
* DRIFT_LAND WAS 0.6 S, AND ONE SEGMENT IS 0.697 S.
|
||||||
|
*
|
||||||
|
* So the target sat INSIDE the newest segment — a point the player very
|
||||||
|
* often did not have yet. It would be dragged up to the edge, run out of
|
||||||
|
* audio, stall, and the stall would trigger another correction. A phone
|
||||||
|
* cannot hold a position closer to live than the segments arriving.
|
||||||
|
*
|
||||||
|
* Three segments back (~2.1 s) is what HLS players choose for themselves on
|
||||||
|
* a live playlist, and it is the smallest target with enough buffered audio
|
||||||
|
* to survive one late segment. The tolerance is wider than one segment too,
|
||||||
|
* so an ordinary arrival wobble is not read as drift.
|
||||||
|
*
|
||||||
|
* It costs about a second and a half of extra delay. A listener a second
|
||||||
|
* further behind is following the service; a listener whose audio keeps
|
||||||
|
* jumping backwards is not.
|
||||||
|
*/
|
||||||
|
var DRIFT_MAX = 3.0;
|
||||||
|
var DRIFT_LAND = 2.1;
|
||||||
|
/* A correction needs time to settle before its own result is judged. */
|
||||||
|
var SEEK_COOLDOWN_MS = 5000;
|
||||||
|
var lastSeekAt = 0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* How far behind the live edge this listener is SUPPOSED to be.
|
* How far behind the live edge this listener is SUPPOSED to be.
|
||||||
@@ -627,6 +656,11 @@
|
|||||||
var want = driftTarget();
|
var want = driftTarget();
|
||||||
var behind = (edge - a.currentTime) - want; // + = too late, - = too early
|
var behind = (edge - a.currentTime) - want; // + = too late, - = too early
|
||||||
if (!force && Math.abs(behind) <= DRIFT_MAX) return;
|
if (!force && Math.abs(behind) <= DRIFT_MAX) return;
|
||||||
|
/* One correction at a time. Without this, timeupdate (four times a
|
||||||
|
second) can fire a fresh seek while the last one is still buffering,
|
||||||
|
and the element never gets a chance to settle anywhere. */
|
||||||
|
if (!force && Date.now() - lastSeekAt < SEEK_COOLDOWN_MS) return;
|
||||||
|
lastSeekAt = Date.now();
|
||||||
/*
|
/*
|
||||||
* Land just BEHIND the target, never on the edge. Seeking exactly to the
|
* Land just BEHIND the target, never on the edge. Seeking exactly to the
|
||||||
* live edge leaves no buffered audio at all, so the phone stalls on the
|
* live edge leaves no buffered audio at all, so the phone stalls on the
|
||||||
|
|||||||
Reference in New Issue
Block a user