Mix.install( [ {:mdex, "~> 0.10"}, {:lumis, "~> 0.1"}, {:phoenix_playground, "~> 0.1"} ], config: [mdex_native: [syntax_highlighter: :lumis]] ) defmodule DemoLayout do use Phoenix.Component def render("root.html", assigns) do ~H""" MDEx Streaming Demo <%= @inner_content %> """ end end defmodule RenderPanel do use Phoenix.LiveComponent def render(assigns) do ~H"""

Rendered Output

<%= if @html == "" do %>
Rendered output will appear here
<% else %>
{Phoenix.HTML.raw(@html)}
<% end %>
""" end end defmodule ChunksPanel do use Phoenix.LiveComponent def render(assigns) do ~H"""

Markdown Chunks

<%= if @chunks == [] do %>
No chunks yet
<% else %>
<%= for chunk <- @chunks do %> <%= String.slice(inspect(chunk), 1..-2//1) %> <% end %>
<% end %>
""" end end defmodule StreamingDemo do use Phoenix.LiveView @mdex_options [ extension: [ alerts: true, autolink: true, footnotes: true, shortcodes: true, strikethrough: true, table: true, tagfilter: true, tasklist: true, math_dollars: true ], parse: [ relaxed_autolinks: true, relaxed_tasklist_matching: true ], render: [ github_pre_lang: true, full_info_string: true, unsafe: true ], syntax_highlight: [formatter: {:html_inline, theme: "github_light"}], streaming: true ] def mount(_params, _session, socket) do {:ok, assign(socket, document: MDEx.new(@mdex_options), chunks: [], html: "", streaming: false, speed: 80, current_chunk: 0, total_chunks: 0, stream_ref: nil )} end def render(assigns) do ~H"""

MDEx Streaming

<%= @speed %>ms
<%= if @total_chunks == 0 do %> Waiting - / - <% else %> Streaming <%= @current_chunk %> / <%= @total_chunks %> <% end %>
<.live_component module={RenderPanel} id="render-panel" html={@html} /> <.live_component module={ChunksPanel} id="chunks-panel" chunks={Enum.take(@chunks, @current_chunk)} />
""" end def handle_event("start_demo", _params, socket) do content = """ # Streaming Music App with Phoenix LiveView :rocket: Launch a collaborative listening room with real-time diffs driven by `Phoenix LiveView`. ## Why LiveView Fits Phoenix LiveView keeps the render pipeline on the server, letting state changes push efficient patches into the session. This means listeners enjoy responsive updates without custom JavaScript, yet you retain declarative Elixir code. ### Experience Goals - :notes: Seamless streaming controls - :headphones: Shared queue awareness - :sparkles: Animated feedback without page reload - :musical_keyboard: Keyboard shortcuts for power users ## Task Board - [x] Initialize project scaffolding - [ ] Wire up LiveView routes - [x] Create router - [ ] Add authentication - [ ] Connect to the audio backend - [ ] Style the listening room - [ ] Publish real-time metrics dashboard ## Architecture Snapshot | Layer | Responsibility | LiveView Hook | | --- | --- | --- | | LiveView | Stateful UI process | mount / handle_event | | PubSub | Broadcast track events | Phoenix.PubSub | | Context | Business logic | StreamMusic.Library | | Presence | Listener roster tracking | Phoenix.Presence | | Data | Persistent playlists | Ecto schemas | ## Setup Command ```bash mix phx.new stream_music --live cd stream_music mix deps.get ``` ## LiveView Outline ```elixir defmodule StreamMusicWeb.PlayerLive do use StreamMusicWeb, :live_view alias Phoenix.PubSub @topic "stream_music:queue" def mount(_params, _session, socket) do if connected?(socket) do Phoenix.PubSub.subscribe(StreamMusic.PubSub, @topic) end {:ok, assign(socket, playlist: [], now_playing: nil, search: "", volume: 60, listeners: %{})} end def handle_event("search", %{"query" => query}, socket) do {:noreply, assign(socket, search: query)} end def handle_event("queue", %{"track" => track}, socket) do Phoenix.PubSub.broadcast(StreamMusic.PubSub, @topic, {:queue, track}) {:noreply, update(socket, :playlist, fn list -> list ++ [track] end)} end def handle_event("play", %{"track" => track}, socket) do Phoenix.PubSub.broadcast(StreamMusic.PubSub, @topic, {:play, track}) {:noreply, assign(socket, now_playing: track)} end def handle_info({:queue, track}, socket) do {:noreply, update(socket, :playlist, fn list -> list ++ [track] end)} end def handle_info({:play, track}, socket) do {:noreply, assign(socket, now_playing: track)} end end ``` ### Progressive Streaming LiveViews start with a static render before upgrading to the persistent connection, so first meaningful paint stays fast while future updates travel over a single channel. ### Performance Metrics The connection latency can be modeled as: $$ L = \\frac{RTT}{2} + P_{server} $$ Where $L$ is total latency, $RTT$ is round-trip time, and $P_{server}$ is server processing time. For optimal user experience, aim for $L < 100ms$ to maintain the illusion of instantaneous updates. ## Interaction Flow 1. Visitor opens the lobby and receives the rendered `PlayerLive`. 2. `mount/3` seeds assigns with playlist snapshots. 3. Track searches call `handle_event/3` to refine results. 4. Queue updates broadcast through PubSub and hydrate everyone. 5. Presence diff pushes listener join or leave events. ### Playlist Signals - **Now Playing**: highlight the active track and waveform - **Queue**: show pending entries with avatars - **History**: list completed tracks for replay fans ### Commands for Library Context Run the generator to scaffold data boundaries: - `mix phx.gen.live Library Track tracks title:string artist:string duration:integer source_url:string` - `mix phx.gen.schema Library.Room rooms slug:string theme:string description:text` ### Queue Feedback | Event | LiveView Callback | Outcome | | --- | --- | --- | | :mag: Search query | `handle_event "search"` | Update suggestions | | :heavy_plus_sign: Queue track | `handle_event "queue"` | Append playlist | | :arrow_forward: Play track | `handle_event "play"` | Change headline state | | :busts_in_silhouette: Presence diff | `handle_info {:presence_diff, diff}` | Refresh listener list | | :checkered_flag: Track finished | `handle_info {:playback_done, track}` | Rotate playlist | ### Listener Journey - Enter lobby and see :headphones: welcome banner - Use instant search to find a favorite song - Add the track to the collaborative queue - Watch :rocket: transitions as the now playing card updates - React with inline emoji to celebrate the vibe - Share with friends via or https://streammusic.app ![App Screenshot](https://placehold.co/600x300/6366f1/white?text=Stream+Music+App) ## Real-Time Considerations > **Important**: Keep these best practices in mind when building real-time features. > > Performance is critical for user experience in collaborative apps. - Keep assigns minimal to avoid large diffs - Stream lists with `Phoenix.LiveView.stream/4` for scalable queues - Push events with `push_event/3` for waveform animations - Use `temporary_assigns` to discard transient payloads - ~~Avoid polling~~ Use LiveView for real-time updates - Balance updates between server broadcasts and client hooks ### Client Integration ```javascript // Subscribe to live updates const socket = new Socket("/socket", {params: {token: userToken}}) socket.connect() const channel = socket.channel("room:lobby", {}) channel.join() .receive("ok", resp => { console.log("Joined successfully", resp) }) .receive("error", resp => { console.log("Unable to join", resp) }) channel.on("new_track", payload => { updateNowPlaying(payload.track) }) ``` ### Deployment Notes :package: Infrastructure Requirements - Configure [CDN edge caching](https://docs.example.com/cdn) for artwork - Enable `live_session` routes for authentication - Tune WebSocket pool size for expected rooms - Leverage clustered nodes for resilient PubSub

💡 Pro Tip: Start with a single node and scale horizontally as your user base grows. Monitor metrics at https://metrics.streammusic.app

## Next Steps 1. Finalize UI polish in Tailwind components 2. Integrate payment tiers for premium rooms 3. Add offline fallbacks when connection drops 4. Extend analytics pipeline for retention insights ## Celebration Wrap the launch with :tada: playlists and a :musical_note: release party! --- *Built with :purple_heart: using [Elixir](https://elixir-lang.org) and [MDEx](https://hex.pm/packages/mdex)* """ chunks = chunk_for_ai_streaming(content) simulate_streaming(socket, chunks) end def handle_event("clear", _params, socket) do socket |> assign( document: MDEx.new(@mdex_options), chunks: [], html: "", streaming: false, current_chunk: 0, total_chunks: 0, stream_ref: nil ) |> then(&{:noreply, &1}) end def handle_event("update_speed", %{"speed" => speed}, socket) do actual_speed = 1001 - String.to_integer(speed) {:noreply, assign(socket, :speed, actual_speed)} end def handle_info({:stream_chunk, ref, chunk}, %{assigns: %{stream_ref: ref}} = socket) do document = Enum.into([chunk], socket.assigns.document) html = html_from(document) socket = socket |> update(:chunks, fn chunks -> chunks ++ [chunk] end) |> assign(:document, document) |> assign(:html, html) |> update(:current_chunk, &(&1 + 1)) {:noreply, socket} end def handle_info({:stream_chunk, _ref, _chunk}, socket), do: {:noreply, socket} def handle_info({:streaming_complete, ref}, %{assigns: %{stream_ref: ref}} = socket) do {:noreply, assign(socket, :streaming, false)} end def handle_info({:streaming_complete, _ref}, socket), do: {:noreply, socket} defp simulate_streaming(socket, chunks) do speed = socket.assigns.speed total_chunks = Enum.count(chunks) stream_ref = make_ref() parent = self() Task.start(fn -> chunks |> Stream.each(fn chunk -> Process.sleep(speed) send(parent, {:stream_chunk, stream_ref, chunk}) end) |> Stream.run() send(parent, {:streaming_complete, stream_ref}) end) updated_socket = assign(socket, %{ document: MDEx.new(@mdex_options), chunks: [], html: "", streaming: true, speed: speed, current_chunk: 0, total_chunks: total_chunks, stream_ref: stream_ref }) {:noreply, updated_socket} end defp html_from(%MDEx.Document{} = document), do: MDEx.to_html!(document, @mdex_options) defp chunk_for_ai_streaming(text) do text |> String.graphemes() |> do_random_chunk([]) end defp do_random_chunk([], acc), do: Enum.reverse(acc) defp do_random_chunk(graphemes, acc) do chunk_size = Enum.random(3..20) {chunk, rest} = Enum.split(graphemes, chunk_size) case chunk do [] -> Enum.reverse(acc) _ -> do_random_chunk(rest, [Enum.join(chunk) | acc]) end end end defmodule DemoRouter do use Phoenix.Router import Phoenix.LiveView.Router pipeline :browser do plug(:accepts, ["html"]) plug(:fetch_session) plug(:put_root_layout, html: {DemoLayout, :root}) plug(:put_secure_browser_headers) end scope "/" do pipe_through(:browser) live("/", StreamingDemo) end end PhoenixPlayground.start(plug: DemoRouter, open_browser: true)