ElixirDose

Journal of the overlooked tiny bits in young programming language. Will update weekly or two, as a way to build the habit of writing something on a regular basis. Authored by Riza and Augie as Editor In Chief.


Building A Simple Stream Media App With Elixir Phoenix Framework

Hey fellow alchemist, how’s going? Long time no see. I’m back! After loooong hiatus. I kinda missed it.

Ok, back to the topic. What will we build today? Well, I have more than one device in my house that I would like to play some movies or musics. And I hate copying stuff from one device to another. And I believe you have one of you have similar problem, or not, whatever. So today we will build some simple streaming media application using Phoenix Web Framework from Elixir language. We will build a web app that list all our media directory so then we can play literally anywhere. As long as you have modern browser and connected to the network.

Prerequisites

First of all, we need Elixir v1.1.1 or later installed and Phoenix v1.1.0 or later. If not, please do install it first. Just go to elixir-lang.org and phoenixframework.org.

Now let’s write our plan to build our media streaming app: 1. Choose an awesome project name 2. Generating the Phoenix project 3. Create listing page for media 4. Create show page to play the media.

That’s pretty much it! Pretty simple, no? Let’s execute!

Choose an awesome project name

Naming things is super hard. So we need to take this task seriously. What should we name this creature? Hhmm.. let see. Let’s take one of the alchemist name in wikipedia. And we got one. We call it Calid Media! Calid Media is a simple media streaming web app for your media center. Named after Alchemist Khalid ibnu Yazid a.k.a Calid.

With the hardest task has done, let’s move to the next task.

Generating the Phoenix project

Now let’s create and generate a new Phoenix project with phoenix.new mix task:

$ mix phoenix.new calid_media
…
Fetch and install dependencies? [Yn] y
* running npm install && node node_modules/brunch/bin/brunch build * running mix deps.get
…

We are all set! To try your new added project do this following command:

$ cd calid_media
$ mix ecto.create
$ mix phoenix.server

Start up the Phoenix server and point the browser to http://localhost:4000 and you’ll see familiar Phoenix placeholder page.

Phoenix Page Placeholder

Cool! Should we proceed? Not just yet. Phoenix shipped with Bootstrap as their go to front-end framework. So we will remove it and replace it with Semantic UI.

Add Semantic UI To The Ingredient

First step we remove web/static/css/app.css then copy all Semantic-UI css files complete with it’s components and themes folder so our web/static/css folder will look something like this:

structure

Now let’s change our layout in web/templates/layout/app.html.eex so it will accommodate semantic-ui style.

<!DOCTYPE html>
<html lang=“en”>
  <head>
<meta charset=“utf-8”>
<meta http-equiv=“X-UA-Compatible” content=“IE=edge”>
<meta name=“viewport” content=“width=device-width, initial-scale=1”>
<meta name=“description” content=“”>
<meta name=“author” content=“”>

<title>Hello CalidMedia!</title>
<link rel=“stylesheet” href=“<%= static_path(@conn, “/css/app.css”) %>”>
  </head>

  <body>
<div class=“ui container”>

  <p class=“alert alert-info” role=“alert”><%= get_flash(@conn, :info) %></p>
  <p class=“alert alert-danger” role=“alert”><%= get_flash(@conn, :error) %></p>

  <main role=“main”>
<%= render @view_module, @view_template, assigns %>
  </main>

</div> <!— /container —>
<script src=“<%= static_path(@conn, “/js/app.js”) %>”></script>
  </body>
</html>

Then let’s check whether semantic-ui works or not. Open up web/templates/page/index.html.eex and remove all the placeholder code and add one or two semantic-ui component.

<h1 class=“ui dividing header”>Calid Media Server</h1>
<button class=“ui big button teal”>Test Semantic Button</button>

Now refresh the browser and let see the big and beautiful teal button shows up!

beautiful button

Awesome! Move on to the next task.

Create Listing Page for Media

To get all media files in specific directory is quite easy in Elixir. We just have to use File.ls function to do that. So without further ado, open up our page controller in web/controllers/page_controller.ex and add the code below.

defmodule CalidMedia.PageController do
  use CalidMedia.Web, :controller

  def index(conn, _params) do
media_dir = “./priv/static/media”
{:ok, files} = File.ls(media_dir)
render conn, “index.html”, files: files
  end
end

Pretty straightforward, no?! First, we choose where the media directory. For our case, just use Phoenix static directory and add new directory called media. Then we iterate through all the files inside priv/static/media directory using Files.ls function and make it accessible from the index.html.eex file. Now go to that web/templates/page/index.html.eex page and make a list of files there.

<h1 class=“ui dividing header”>Calid Media Server</h1>
<button class=“ui button teal”>Test Semantic Button</button>

<ul>
  <%= for file <- @files do %>
  <li><%= file %></li>
  <% end %>
</ul>

Easy! Let see how it goes!

file list

Add Link When Click The File List

Wonderful! Every single file got listed. We will filter that later. For now, let’s add link to the file list so later we can play the media. But because we haven’t show or detail page, let s link the file to index page :) We will change that later.

<h1 class=“ui dividing header”>Calid Media Server</h1>

<ul>
  <%= for file <- @files do %>
  <li><%= link file, to: page_path(@conn, :index) %></li>
  <% end %>
</ul>

Filter The File

Right now, all files will appear on the list. So let’s filter it to only media file, specifically let’s limit to mp4 and mp3 file type.

To do that, we will use Enum.filter function iterate through our file list and remove unnecessary files.

defmodule CalidMedia.PageController do
  use CalidMedia.Web, :controller

  def index(conn, _params) do
media_dir = “./priv/static/media”
filetype = [“.mp4”, “.mp3”]
{:ok, files} = File.ls(media_dir)
filtered_files = Enum.filter(files, fn(file) -> String.ends_with?(file, filetype) end)
render conn, “index.html”, files: filtered_files
  end
end

We also use String.ends_with? function to take filename with this kind of format filename.mp3 and check whether the extension is .mp3 or .mp4.

And now, if we refresh the browser, all non media files just gone.

file list link

Redirect To Show Page

After filtering done, let’s take the link and redirect it to a new page so we can play our video or music. Change the link inside web/templates/index.html.eex to page called show.

<h1 class=“ui dividing header”>Calid Media Server</h1>

<ul>
  <%= for file <- @files do %>
  <li><%= link file, to: page_path(@conn, :show, file) %></li>
  <% end %>
</ul>

After you save your file, take a look at the browser. It will complain that there is no :show action, yet.

error message

Don’t you worry. Just add one inside our page controller web/controllers/page_controller.ex.

defmodule CalidMedia.PageController do
  use CalidMedia.Web, :controller

  def index(conn, _params) do
…
  end

  def show(conn, file) do
IO.inspect(file)
  end
end

For now, just print out to the terminal the file parameter we’ve got from index page using IO.inspect or IO.puts. And also don’t forget to add show in our route file web/router.ex.

  scope “/“, CalidMedia do
pipe_through :browser # Use the default browser stack

get “/“, PageController, :index
get “/show”, PageController, :show
  end

Now if we refresh the page, it’s still error. But different error. Meaning, we’re progressing.

enum

As you can see, Phoenix complains that we need some kind of struct for params that we’ve sent from index to show page. Please do correct me if I’m wrong here. So we need to define our data struct first. We will do it on web/models/media.ex file cause we want the file and filename act as our model.

defmodule CalidMedia.Media do
  @derive {Phoenix.Param, key: :filename}
  defstruct [:filename]
end

Pretty simple, our model or struct just have one component called :filename and derived from Phoenix.Param. More on this, please do read this blog post. Next thing to do is to convert list of files in page controller into struct that we define. And we change our show action as well to accommodate this changes.

defmodule CalidMedia.PageController do
  use CalidMedia.Web, :controller

  def index(conn, _params) do
media_dir = “./priv/static/media”
{:ok, files} = File.ls(media_dir)
filtered_files = Enum.filter(files, fn(file) -> String.ends_with?(file, [“.mp4”, “.mp3”, “.avi”]) end)
struct_files = Enum.map(filtered_files, fn(file) -> %CalidMedia.Media{filename: file} end )
render conn, “index.html”, files: struct_files
  end

  def show(conn, %{“filename” => filename}) do
render conn, “show.html”, filename: filename
  end
end

As you can see, after filtering the file type, then we generate list of struct in struct_files variable using Enum.map to convert filename into CalidMedia.Media struct.

In show action, we just have to accept params as a struct. Then we render the html alongside filename variable.

And now, our index page looks ok again.

back to the game

But when you click one of the file in the list, we will get new error message.

another one

And it make sense. We have to create web/templates/page/show.html.eex so Phoenix can render it. Let’s do so.

<h1 class=“ui dividing header”>Play Calid Media Server</h1>

<video width=“100%” height=“100%” controls>
  <source src=“/media/<%= @filename %>” type=“video/mp4”>
  Your browser does not support the video tag. Just replace it.
</video>

Here we just use html5 video tag <video> to play the particular video. That’s enough for now.

video

See that video and it’s playable! Awesome!!

Playing Audio or Video

Right at this moment, we’ve just play everything in <video> tag. Well, if you’re trying to play audio file in mp3 format, it’s actually playable as well.

audio but video player

But for the purpose of our learning, let’s check if the file type is mp3 then we load <audio> tag instead. For that, we need view helper to get the extension of the file. Open up web/views/page_view.ex and we will split the filename string to get the file extension.

defmodule CalidMedia.PageView do
  use CalidMedia.Web, :view

  def filetype(filename) do
typeoffile = filename
  |> String.split(“.”)
  |> Enum.at(1)

cond do
  typeoffile == “mp3” -> “mpeg”
  typeoffile == “mp4” -> “mp4”
end
  end
end

After that, we just have to load audio player if the file is audio and video player if the file is video.

<h1 class=“ui dividing header”>Play Calid Media Server</h1>

<%= if filetype(@filename) == “mp4” do %>
<video width=“100%” height=“100%” controls>
  <source src=“/media/<%= @filename %>” type=“video/<%= filetype(@filename) %>”>
  Your browser does not support the video tag. Just replace it.
</video>
<% else %>
<audio controls>
  <source src=“/media/<%= @filename %>” type=“audio/<%= filetype(@filename) %>”>
  Your browser does not support the audio element.
</audio>
<% end %>

Now, we can play both with appropriate player :)

video

audio

Boom! Here you go! One little touch maybe. Just add back button in the show template to get back to the list page.

<h1 class=“ui dividing header”>Play Calid Media Server</h1>

<%= if filetype(@filename) == “mp4” do %>
<video width=“100%” height=“100%” controls>
  <source src=“/media/<%= @filename %>” type=“video/<%= filetype(@filename) %>”>
  Your browser does not support the video tag. Just replace it.
</video>
<% else %>
<audio controls>
  <source src=“/media/<%= @filename %>” type=“audio/<%= filetype(@filename) %>”>
  Your browser does not support the audio element.
</audio>
<% end %>

<div class=“ui horizontal divider”>&nbsp;</div>

<%= link “Back”, to: page_path(@conn, :index), class: “ui positive button” %>

From this moment on, whenever you have media files, just drop it to the destination directory then fired up Phoenix server and you can enjoy watching or listening literally anywhere. As long as you have modern browser in your device.

Conclusion

In this journey, we did learn a lot. We learn how to get rid of Bootstrap and use Semantic-UI as the replacement. Then we learn how to get list of files in certain directory. After that we learn how to ‘encapsulate’ the data using struct for our parameter. Then we learn how to convert list into list of struct.

We also learn how to make a helper function inside the view. Last but not least, we learn how to use condition using cond and if statement respectively.

This app is far from complete. We can generally make the list pretty with thumbnail or something. It would be better as well if we can take media directory outside of this Phoenix app. But leave that for the next part, will you :)

All code available over here on github.

Back