fix(web/session): feed asciinema-player inline data, not a Blob URL
SessionDrawer built a cast blob, pushed it through URL.createObjectURL,
and passed the blob URL to AsciinemaPlayer.create(). That's racy with
useEffect's cleanup: each new page of events re-fires the effect, the
cleanup revokes the URL, and the player's already-in-flight async
loadRecording() lands on a dead URL with no visible error — result was
a centered play button with an empty black pane, playback never starts.
asciinema-player v3's recording driver accepts {data: <string>} as a
first-class source (see core-DnNOMtZn.js:905-930 doFetch — string/
ArrayBuffer data is wrapped in `new Response(value)` and handed to the
parser). Skip the blob detour entirely, pass the cast text inline.
Also filter events to valid asciicast channels (o/i/r) before feeding
so a future stray SD field can't derail the parser, and log mount
errors to console for next-time debugging.
This commit is contained in:
@@ -117,22 +117,37 @@ const SessionDrawer: React.FC<SessionDrawerProps> = ({ decky, sid, fields, onClo
|
||||
|
||||
// Re-mount the player whenever the event window grows. asciinema-player
|
||||
// doesn't expose a public feed() API in v3, so we rebuild from the full
|
||||
// in-memory blob each time — cheap for v1-scale sessions (≤ 10 MB cap).
|
||||
// in-memory cast each time — cheap for v1-scale sessions (≤ 10 MB cap).
|
||||
//
|
||||
// Pass the cast as {data: ...} directly rather than a Blob URL. The
|
||||
// URL path silently fails when the browser's fetch for the blob races
|
||||
// the createObjectURL revoke, or when the mime-type guess trips the
|
||||
// player's loader — either way the user gets a play button that does
|
||||
// nothing on click. Inline data skips the whole fetch detour.
|
||||
useEffect(() => {
|
||||
if (!header || !playerContainer.current || events.length === 0) return;
|
||||
if (!header || !playerContainer.current) return;
|
||||
// Asciicast v2 ch values: "o" (output), "i" (input), "r" (resize).
|
||||
// Drop anything else so a stray malformed line can't derail parsing.
|
||||
const playable = events.filter(([, ch]) => ch === 'o' || ch === 'i' || ch === 'r');
|
||||
if (playable.length === 0) return;
|
||||
|
||||
if (playerInstance.current) {
|
||||
try { playerInstance.current.dispose(); } catch { /* ignore */ }
|
||||
playerInstance.current = null;
|
||||
}
|
||||
const cast = buildCastBlob(header, events);
|
||||
const blob = new Blob([cast], { type: 'application/x-asciicast' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
playerInstance.current = AsciinemaPlayer.create(url, playerContainer.current, {
|
||||
fit: 'width',
|
||||
terminalFontSize: '12px',
|
||||
});
|
||||
const cast = buildCastBlob(header, playable);
|
||||
try {
|
||||
playerInstance.current = AsciinemaPlayer.create(
|
||||
{ data: cast },
|
||||
playerContainer.current,
|
||||
{ fit: 'width', terminalFontSize: '12px' },
|
||||
);
|
||||
} catch (err) {
|
||||
// Surface to console so we have a fingerprint next time a cast
|
||||
// parses as "valid" but refuses to play.
|
||||
console.error('asciinema-player failed to mount', err);
|
||||
}
|
||||
return () => {
|
||||
URL.revokeObjectURL(url);
|
||||
if (playerInstance.current) {
|
||||
try { playerInstance.current.dispose(); } catch { /* ignore */ }
|
||||
playerInstance.current = null;
|
||||
|
||||
Reference in New Issue
Block a user