RudolfVonKrugstein

RudolfVonKrugstein

Serving a file while it is being created in memory using chunked transfer

Hi,

I am new to elixir and phoenix and I am trying to solve the following problem:

In my application I create an audio file (using a tts system) that takes a while to create (up to several seceonds).

I want http clients (well, the browser) to be apple to get/stream that file:

  • while it is created
  • after it is created

I build a solution for this, but it feels kind of complicated. As I am an absolute beginner and not very experienced on how to build good solutions with elixir, I wonder if someone could give me some input on how to better solve this, or maybe even use existing solutions that I am not aware of. So maybe someone has some input on this?

My solution consists of:

  • an audio file provider, hat creates the audio file form tts
  • an Audio Buffer, that holds the audio created to far in memory. This is implemented as a GenServer.
  • an Audiofile Controller, that works as a phoenix controller and serves the audio file to the browser using chunked transfer

The Process is now like this:

  1. The TTS Creator starts working and sends data to the Audio Buffer GenServer
  2. The Browser requests the file from the Audiofile controller
  3. The controller ask the Audio Buffer for the data so far and …
  4. Receive the buffer with the data so far, which the controller sends to the browser.
  5. If the file is not yet finished, the Audiofile Controller has to receive the remaining data as it is created. For this I use the Phoenix PubSub on the “audiofile” topic, to which the Audiofile Controller subscribes and sends to the Browser the received data to the Browser.
┌───────────────┐    1    ┌──────────────────────┐                   
│               │  send   │                      │   5 send more data
│  TTS Creator  ├─────────►     Audio Buffer     ├───────────┐       
│               │  data   │                      │           │       
└───────────────┘         └────────▲────┬────────┘           │       
                          3 ask for│    │ 4 send        ┌────▼────┐  
                            data   │    │ data buffer   │ PubSub  │  
                       ┌───────────┴────▼─────────┐     └────┬────┘  
                       │                          │          │       
                       │   Audiofile Controller   ◄──────────┘       
                       │                          │                  
                       └─────────────▲────────────┘                  
                                     │2                              
                                     │Download AudioFile             
                              ┌──────┴─────┐                         
                              │            │                         
                              │  Browser   │                         
                              │            │                         
                              └────────────┘                         

For this I have implemented the AudioController like this (simplified pseudo code):

defmodule MyAppWeb.AudiofileController do
  use MyAppWeb, :controller
  require Logger

  def show(conn, %{"fileid" => file_id}) do
    # make sure we dont miss any messages ...
    MyAppWeb.Endpoint.subscribe("audiofiles")

    case FileStorage.get_file_data(file_id) do
      {:ok, data} ->
        conn = send_chunked(conn, 200)

        send_chunked_file(conn, file_id)
      # other cases
    end
  end

  def send_chunked_file(conn, file_id) do
    receive do
      %Phoenix.Socket.Broadcast{
        topic: "audiofiles",
        event: "filedata",
        payload: {:file_data, name, new_data}
      }
      when name == file_id ->
        conn
        |> chunk(new_data)

        send_chunked_file(conn, file_id)

      %Phoenix.Socket.Broadcast{
        topic: "audiofiles",
        event: "finish",
        payload: {:finish_file, name}
      }
      when name == file_id ->
        # we are done
        conn
    after
      5_000 ->
        Logger.error("Waiting for file #{file_id} timed out")
        conn
    end
  end
end

Especially the fiddling around with the PubSub Messages feels a little, like it could be better. Also this design means, that if multiple files are created in parallel, the PubSub messages from the Audio Buffer reach all Audiofile Controllers, even those serving other files than the one the PubSub message is for.

Does anyone have some Ideas for an more idiomatic way of doing this?

Thank you guys!

Most Liked

Hermanverschooten

Hermanverschooten

If you send self() to the function that now broadcasts on the pubsub, you can use Process.send/3 to send back the data, skipping the pubsub altogether and avoid that issue.

Where Next?

Popular in Questions Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
nobody
How to bind a phoenix app to a specific ip address? could not find anything about that, nowhere, unfortunately, but for me this is quite...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

Other popular topics Top

chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52341 488
New
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31142 143
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New

We're in Beta

About us Mission Statement