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
|
// 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
|
// 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(() => {
|
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) {
|
if (playerInstance.current) {
|
||||||
try { playerInstance.current.dispose(); } catch { /* ignore */ }
|
try { playerInstance.current.dispose(); } catch { /* ignore */ }
|
||||||
playerInstance.current = null;
|
playerInstance.current = null;
|
||||||
}
|
}
|
||||||
const cast = buildCastBlob(header, events);
|
const cast = buildCastBlob(header, playable);
|
||||||
const blob = new Blob([cast], { type: 'application/x-asciicast' });
|
try {
|
||||||
const url = URL.createObjectURL(blob);
|
playerInstance.current = AsciinemaPlayer.create(
|
||||||
playerInstance.current = AsciinemaPlayer.create(url, playerContainer.current, {
|
{ data: cast },
|
||||||
fit: 'width',
|
playerContainer.current,
|
||||||
terminalFontSize: '12px',
|
{ 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 () => {
|
return () => {
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
if (playerInstance.current) {
|
if (playerInstance.current) {
|
||||||
try { playerInstance.current.dispose(); } catch { /* ignore */ }
|
try { playerInstance.current.dispose(); } catch { /* ignore */ }
|
||||||
playerInstance.current = null;
|
playerInstance.current = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user