Reactive Media Player

You should read the Playing Music tutorial before this one.

Since version 4, Eliom embeds React library in order to use reactive html element like nodes and attributes. The documentation for the the React library itself can be found here.

The final eliom code is available for download.

Basics

A reactive element, or more generally a reactive value, depends on the current value of a signal. For instance :

{shared{
open Eliom_content
open Html5
}}

{client{
let s, set_s = React.S.create 0 (* signal creation *)
}}

{shared{
let example_div () =
  C.node {{R.pcdata (React.S.map string_of_int s)}}

let incr_button =
  D.(button
      ~button_type:`Button
      ~a:[a_onclick {{fun _ -> set_s (succ (React.S.value s))}} ]
      [pcdata "Increment"])
}}

The signal s carries an int value initialized at 0 and set_s is the update function generating an occurence of the signal.

The example_div value is a div containing a string and all of this depends of the value of s.

The magic part : we never have to write explicitly the update of the div. Its behavior is described in it's own code and not in the code of the button.

Functional Reactive Media Player

This part explains how to create a simple media player like in Playing Music tutorial but with custom controls. We are going to write those in FRP (Functionnal Reactive Programming) way.

In order to show a short tutorial, we only create three controls: Play, Pause and Seek/Progress bar. So, let's write the corresponding type:

{shared{
open Eliom_content
open Html5

type media_action = Play | Pause | Seek of float
}}
{client{

let media_s, set_media_s = React.S.create Pause

}}

Each html element emits a signal value corresponding to it's action. It's enough to create our Play and Pause inputs.

{server{

let pause_button () =
  D.(button
      ~button_type:`Button
      ~a:[a_onclick {{ fun _ -> set_media_s Pause }}]
      [pcdata "Pause"])

let play_button () =
  D.(button
      ~button_type:`Button
      ~a:[a_onclick {{ fun _ -> set_media_s Play }}]
      [pcdata "Play"])

}}

To use our buttons, we now create a media (audio or video) html element on the server side.

{server{
let media_uri =
  Html5.D.make_uri
    ~service:(Eliom_service.static_dir ())
    ["hb.mp3"]
    (* Assuming that you add a file 'example.mp3' in the
       static directory of your project *)

let media_tag () =
  let media = D.(audio ~src:media_uri [pcdata "alt"]) in
  let _ = {unit{
    Lwt.async (fun () ->
      let media = To_dom.of_audio %media in
      let media_map = function
        | Play -> media##play ()
        | Pause -> media##pause ()
        | Seek f -> media##currentTime <- (f /. 100. *. media##duration)
      in Lwt_react.S.keep (React.S.map media_map media_s) ;
      Lwt.return ()
      )}}
  in media

}}

The code in {unit{...}} is on the client part. It's an asynchronous Lwt thread that executes the code inside: mapping a function media_action -> unit to the signal media_s.

{server{
module React_Player_app =
  Eliom_registration.App
    (struct
      let application_name = "react_player"
    end)

let media_service =
  Eliom_service.App.service ~path:[] ~get_params:Eliom_parameter.unit ()

let () =
  React_Player_app.register
    ~service:media_service
    (fun name () ->
       let body =
         D.(body [
             h2 [pcdata "Media"];
             media_tag ();
             div [play_button (); pause_button ()]
           ])
       in
       Lwt.return (Eliom_tools.D.html ~title:"Media" ~css:[] body)
    )

}}

Now you should have an html node with an audio tag and two buttons play and pause. The progress bar is slightly harder to understand, but thanks to FRP, very easy to write. It's basically an input with range type. In our program, the progress bar must emit the signal media_s with the value Seek f at input handling. Then, it must evolve during the progression of the media (we need an other signal). To conclude, we must check that the display (the value) of the progress bar is not modified when the user is seeking.

{client{

let progress_s, set_progress_s = React.S.create (0., 0.)
let unblock_s, set_unblock_s = React.S.create true

}}


{server{

let progress_bar () =
  let progress_value =
    {float React.signal{
        let f (time, duration) =
          if duration = 0. then 0. else time /. duration *. 100.
        in React.S.map f progress_s
    }} in
  let attrs = D.([
    a_input_min 0.;
    a_input_max 100.;
    a_onmousedown {{fun _ -> set_unblock_s false}};
    a_onmouseup {{fun _ -> set_unblock_s true}};
    C.attr {{R.a_value (React.S.map (Printf.sprintf "%0.f")
                          (React.S.on unblock_s 0. %progress_value))}}
    ])
  in
  let d_input = D.(float_input ~input_type:`Range () ~value:0. ~a:attrs) in
  let _ = {unit{
    Lwt.async (fun () ->
      let d_input = To_dom.of_input %d_input in
      Lwt_js_events.inputs d_input (fun _ _ ->
        set_media_s (Seek (Js.parseFloat d_input##value)) ;
        Lwt.return ()
      ))}}
  in d_input

}}

To end this tutorial, you can add a progress_bar () call inside the div containing play and pause. We also need a mechanism which emits the progress_s signal. We modify the media tag with an eventhandler on timeupdate.

let media_tag () =
  let media = D.(audio ~src:media_uri [pcdata "alt"]) in
  let _ = {unit{
    Lwt.async (fun () ->
      let media = To_dom.of_audio %media in
      let media_map = function
        | Play -> media##play ()
        | Pause -> media##pause ()
        | Seek f -> media##currentTime <- (f /. 100. *. media##duration)
      in Lwt_react.S.keep (React.S.map media_map media_s) ;
      Lwt_js_events.timeupdates media (fun _ _ ->
        set_progress_s (media##currentTime, media##duration) ;
        Lwt.return ()
      ))}}
  in media

Exercises

  • Add a control to set the volume
  • Add an Eliom_bus to control several clients