-- Leo's gemini proxy

-- Connecting to bbs.geminispace.org:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini; charset=utf-8

I saw a comment by @clseibold earlier (hope I spelled that right) talking about streaming audio capabilities in mobile Gemini clients. I can't seem to find the post anymore, but it got me thinking about whether Opal (the client library that Rosy Crow uses to do it's Gemini stuff) can support that.


Currently it does not support that, because the entire response is read all at once into a buffer.


Thinking about this a while, I of course arrived at the classic conclusion that distinguishing between a slow connection and an intentional, long-lived stream is a Difficult Problem™.


(This isn't a new discovery of course; I know there's no shortage of folks whinging about Gemini's lack of a Content-Length equivalent.)


As far as Opal is concerned, I can certainly bolt-on a separate method that exposes the raw response stream unmolested. I'd still like to keep the current behavior, as it enables the library's typed document model (i.e. looping through a collection of ILine objects that represent the gemtext).


Anyhow, this gave me some food for thought and tinkering with a proof-of-concept was a nice way to spend a Sunday afternoon. 🍺


🐝 Addison

2023-12-04 · 5 months ago · 👍 skyjake


10 Comments ↓


😈 dimkr · Dec 04 at 06:25:

It's a problem because the protocol doesn't allow you to distinguish between the two use cases. In gplaces, I just let the user decide (save gemini://... vs save gemini:// -, the former means "download and open with program for this kind of files" while the latter means "stream to stdin of that program").


🕹️ skyjake [...] · Dec 04 at 07:13:

When it comes to Lagrange, all requests are handled as streaming. That is, when a request is ongoing, incoming data is handled as it arrives and processed incrementally when possible.


However, currently there is a significant inefficiency with page layout. Whenever new data arrives, even if it's just a single byte, the entire page layout gets redone. This can mean lots of unnecessary work (measuring text runs, wrapping lines, etc.). I've been planning to refactor the document class to allow appending new lines to an existing layout, but it is not a trivial change.


Streaming media has some additional challenges because it depends on the codec and its API whether it's possible to provide the data as a stream. Often the APIs assume that streams are coming over HTTP(S) or some other bespoke streaming protocol, and that's a nice ready-made solution if you doing a web-based program. Decoding a Gemini stream in practice means streaming from a memory buffer managed by the client, which is a little more complicated.


😈 dimkr · Dec 04 at 07:46:

@clseibold The main difference is that some file formats require you to have the whole thing before you can show something, while others don't. gplaces streams gemtext to the screen and it can stream any content as long as the handler program accepts "-" as the file path and reads from stdin, but some applications that support this don't show the file immediately, only when they finish reading the whole thing. As far as I know, the "anything can be streamed" assumption is false and you'll have to handle this on a MIME type by MIME type basis.


😈 dimkr · Dec 04 at 10:23:

@clseibold Yes, but not making this distinction means a UX problem: the user needs to know if the client is waiting for the end of the file (through a progress bar?) or filling a buffer before it can show something (a spinner?) or the buffer is empty because download has stalled (a spinner?) or everything is OK but the client is still receiving more data while displaying something. The client can delegate the work of displaying a file to some other component, be it a separate application or not, but it can be confusing for the user if the download part and the display part don't communicate so the client can't explain what's going on.


🐝 Addison [OP] · Dec 04 at 18:20:

Rosy Crow runs into a problem similar to what @skyjake described. It's using the native Android WebView to render page contents, so all incoming Gemtext must first be transformed into a valid HTML document and sent to the embedded browser. Since HTML cannot be appended-to in a simple fashion, this means that the whole document has to be re-generated.


One workable solution in this case would be to add some javascript to manipulate the DOM as new content arrives. I will likely end up doing this at soem point, but it sounds like it will be a PITA to test.


Along the lines of what @dimkr was mentioning: while the matter of a malicious/slow server seems like an irrelevant corner-case; it's one of those problems that throws a wrench into the whole concept of just waiting for the server to close the connection. It also adds significant complexity to the UI/UX side of things:


If a host is drip-feeding me bytes:

How long do I wait until I ask the user whether they want to cancel the request?

Will it get annoying if I do that for every slow response, even on a site with lots of streaming?

What if the user indicated they want to stream, but the server then *actually* fails for some other reason and never kills the connection?


😈 dimkr · Dec 05 at 06:33:

@Addison you can't distinguish between a troll server that sends the document x bytes at a time to annoy you, and a server that "streams" the response in chunks but sometimes network latency is higher or reliability is lower. And this is also true for streaming of audio and video, not just gemtext. The only "solution" is having socket timeouts (for sending and receiving) so the transfer fails if nothing is received (or acknowledged) for some time, but also per-session timeout (so download fails after a while if the server keeps sending more data but the file is really big or endless).


😈 dimkr · Dec 05 at 17:43:

@clseibold Maybe TCP keepalives, but a client may close a session after some deadline regardless of last I/O time, or terminate sessions based on time since last I/O (last packet carrying more data to display) instead of time since the last packet (including keepalive packets). Gemini is not really designed for streaming, it can be done but you need to make assumptions about the client and that means a capsule marked 'works best with x', like old sites built with IE in mind.


😈 dimkr · Dec 06 at 07:53:

@clseibold I see things differently, I think that supporting a MIME type is one thing, but assumptions about client behavior is another. For example, some capsules have links that align nicely in Lagrange, and Station expects a client certificate for / although it's /join that returns 60 (Lagrange defaults to certificates for /, so it "just works"). In gplaces, I had to mimic Lagrange in some corner cases - for example, only warn, not show an error, if a capsule doesn't send close_notify like the spec says. I don't like capsules that work better with a specific client, or only with a specific client.


🕹️ skyjake [...] · Dec 06 at 19:38:

Yes you are correct about the history @clseibold.


This reminds me I should use the "/?register" solution myself in Bubble, because having an actual "Create Account" UI action beats the current "just activate a certificate but please only at the root".


😈 dimkr · Dec 07 at 07:23:

@clseibold @skyjake This history shows how much "wiggle room" the spec has, allowing clients to behave very differently from each other, until clients and servers achieve some status quo not mandated by the spec. I know that my client (and probably many others) doesn't support "chat" because it previews the document during download but shows the prompt that allows you to navigate to a link only when transfer is finished.

-- Response ended

-- Page fetched on Sun May 19 17:12:53 2024