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 :

{server{

open Eliom_content

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

let example_div =
  C.node {{R.node (React.S.map (
    fun s_value ->
      Html5.D.(div [pcdata (string_of_int s_value)]
    )) s)}}

let incr_button =
  Html5.D.(string_input ~input_type:`Submit () ~value:"Increment"
    ~a:[a_onclick (fun _ -> set_s (succ (React.S.value s)))])
}}

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
}}
{client{

type media_action = Play | Pause | Seek of float
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 ()) ["example.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
      Lwt_react.S.keep (React.S.map (function
        | Play -> media##play ()
        | Pause -> media##pause ()
        | Seek f -> media##currentTime <- (f /. 100. *. media##duration))
        media_s);
      Lwt_js_events.timeupdates media (ontimeupdate_emit media);
      )}}
  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 () ->
      Lwt.return D.(
        Eliom_tools.D.html ~title:"Media" ~css:[]
          (body [
            h2 [pcdata "Media"];
            media_tag ();
            div [
              play_button (); pause_button ()]])))
}}

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.