A Literate Ur/Web Adventure

by Daniel Patterson on May 21, 2013

Ur/Web is a language / framework for web programming that both makes it really hard to write code with bugs / vulnerabilities and also makes it really easy to write reactive, client-side code, all from a single, simple, codebase. But it is built on some pretty deep type theory, and while it is an incredibly practical research project, some corners of it still show - like error messages that scroll pages off the screen. I’ve experimented with it before, and have written a small application that is beyond a demo, but still small enough to be digestible.

For completeness and clarity, I present it here in complete literate style - all the files, interspersed with comments, are presented. They are split into sections by file, which are named in headings. All the text between the file name and the next file name that is not actual code is within comments (that is what the #, (* and *) are for), so you can copy the whole thing to the files and build the project. All the files should go into a single directory. It builds with the current version of Ur/Web. You can try out the application, as it currently exists (which might have been changed since writing this), at lab.dbpmail.net/dn. The full source, with history, is available at hub.darcs.net/dbp/dnplayer.

The application is a video player for the daily news program Democracy Now!. The main point of it is to remember where in the show you are, so you can stop and resume it, across devices. It should work on desktop and mobile applications - I have targetted Chrome on Android, Chrome on computers, and Safari on iPhones/iPads. The main reason for not supporting Firefox is that it does not support the (proprietary) video/audio codecs that are the only format that Democracy Now! provides.

dn.urp

# .urp files are project files, which describe various meta-data about
# Ur/Web applications. They declare libraries (like random, which we'll
# see later), information about the database (both what it is named and
# where to generate the sql for the tables that the application is using).
# They separate meta-data declarations from the modules in the project by
# a single blank line, which is why we have comments on all blank lines
# prior to the end.
library random
database dbname=dn
sql dn.sql
# 
# They also allow you to rewrite urls. By default, urls are generated
# consistently as Module/function_name, which means that the main
# function inside Dn, our main module, is our root url. We can rewrite
# one url to another, but if we leave off the second, that rewrites to
# root. We can also strip prefixes from urls with a rewrite with a *.
# 
rewrite url Dn/main 
rewrite url Dn/*
# 
# safeGet allows us to declare that a function is safe to generate urls
# to, ie that it won't cause side effects. Along the same safety lines,
# we declare the external urls that we will generate and scripts we will
# include - making injecting resources hosted elsewhere hard (as Ur/Web
# won't allow you to create urls to anything not declared here).
#
# 
safeGet player
allow url http://dncdn.dvlabs.com/ipod/*
allow url http://traffic.libsyn.com/democracynow/*
allow url http://dbpmail.net/css/default.css
allow url http://dbpmail.net
allow url http://hub.darcs.net/dbp/dnplayer
allow url http://democracynow.org
allow url http://lab.dbpmail.net/dn/main.css
script http://lab.dbpmail.net/static/jquery-1.9.1.min.js
# One odd thing - Ur/Web doesn't have a static file server of its own, so
# you need to host any FFI javascript elsewhere. Here's where the javascript for
# this application, presented later, is hosted. For trying it out, leaving
# this the same is fine, though if you want to change the javascript, or
# not depend on my copy being up, you should change this and the reference in
# the application.
script http://lab.dbpmail.net/dn/dn.js
# 
# Next, we declare that we have foreign functions in a module called dnjs. This
# refers to a header file (.urs), and we furthermore declare what functions within
# it we are using. We declare them as effectful so that they aren't called multiple
# times (like Haskell, Ur/Web is purely functional, so normal, non-effectful functions are not
# guaranteed to be called exactly once - they could be optimized away if the compiler
# did not see you use the result of the function, and could be inlined (and thus
# duplicated) if it would be more efficient).
# 
ffi dnjs
jsFunc Dnjs.init=init
effectful Dnjs.init
jsFunc Dnjs.set_offset=set_offset
effectful Dnjs.set_offset

# The last thing we declare is the modules in our project. $/ is a prefix that means to
# look in the standard library, as we are using the option type (Some/None in OCaml/ML,
# Just/Nothing in Haskell, and very roughly a safe null in other languages). sourceL is
# a helper for reactive programming (to be discussed later). And finally, our main module,
# which should be last.
#         
$/option
sourceL
dn

dn.urs

(*

.urs files are header files (signature files), which declare all the public functions in the module (in this case, the Dn module). We only export our main function here, but all functions that have urls that we generate within the applications are also implicitly exported.

The type of main, unit -> transaction page, means that it takes no input (unit is a value-less value, a placeholder for argumentless functions), and it produces a page (which is a collection of xml), within a transaction. transaction, like Haskell’s IO monad, is the way that Ur/Web handles IO in a safe way. If you aren’t familiar with IO in Haskell, you should go there and then come back.

*)
val main : unit -> transaction page

random.urp

# Random is a simple wrapper around librandom to provide us with random
# strings, that we use for tokens. We included it above with the line
# `library random`. Libraries are declared with separate package files,
# and here we link against librandom.a, include the random header, and declare
# that we are using functions declared in random.urs (that is the ffi line).
# We also declare that all three functions are effectful, because they have
# side effects
#
# NOTE: It has been pointed out that instead of doing this, we could either:
#       A. use Ur/Web's builtin `rand` function, and construct the strings
#          without using the FFI, or even easier:
#       B. just use the integers than `rand` generates as tokens.
#
#       I didn't realize that `rand` existed when I wrote this, but I'm leaving
#       it in because it is a (concise) introduction to the FFI, which, given
#       the relatively small body of Ur/Web libraries, is probably something
#       you'll end up using if you build any large applications.
effectful Random.init
effectful Random.str
effectful Random.lower_str
ffi random
include random.h
link librandom.a

random.urs

(*

Like with main, we see that the signatures of these functions are ‘transaction unit’ and int -> transaction string, which means the former takes no arguments, and the latter two take integers (lengths), and produce strings, within transactions. They are within transaction because they create side effects (ie, if you run them twice, you will likely not get the same result), and thus we want the compiler to treat them with care (as described earlier). Init seeds the random number generator, so it should be called before the other two are

*)
val init: transaction unit
val str : int -> transaction string
val lower_str : int -> transaction string

random.h

/*

Here we have the header file for the C library, which declares the same signatures as above, but using the structs that Ur/Web uses, and the naming convention that it expects (uw_Module_name).

*/
#include "types.h"

uw_Basis_unit uw_Random_init(uw_context ctx);
uw_Basis_string uw_Random_str(uw_context ctx, uw_Basis_int len);
uw_Basis_string uw_Random_lower_str(uw_context ctx, uw_Basis_int len);

random.c

/*

And finally the C code to generate random strings.

*/
#include "random.h"
#include <stdlib.h>
#include <time.h> 
#include "urweb.h"

/* Note: This is not cryptographically secure (bad PRNG) - do not
   use in places where knowledge of the strings is a security issue.
*/

uw_Basis_unit uw_Random_init(uw_context ctx) {
  srand((unsigned int)time(0));
}

uw_Basis_string uw_Random_str(uw_context ctx, uw_Basis_int len) {
  uw_Basis_string s;
  int i;

  s = uw_malloc(ctx, len + 1);

  for (i = 0; i < len; i++) {
    s[i] = rand() % 93 + 33; /* ASCII characters 33 to 126 */
  }
  s[i] = 0;

  return s;
}

uw_Basis_string uw_Random_lower_str(uw_context ctx, uw_Basis_int len) {
  uw_Basis_string s;
  int i;

  s = uw_malloc(ctx, len + 1);

  for (i = 0; i < len; i++) {
    s[i] = rand() % 26 + 97; /* ASCII lowercase letters */
  }
  s[i] = 0;

  return s;
}

dn.ur

(*

We’ll now jump into the main web application, having seen a little bit about how the various files are combined together. The first thing we have is the data that we will be using - one database table, for our users, and one cookie. The tables are declared with Ur/Web’s record syntax, where Token, Date, and Offset are the names of fields, and string, string, and float are the types.

All tables that are going to be used have to be declared, and Ur/Web will generate SQL to create them. This is, in my opinion, one weakness, as it means that Ur/Web doesn’t play well with others (as it needs the tables to be named uw_Module_name), and, even worse, if you rename modules, or refactor where the tables are stored, the names of the tables need to change - if you are just creating a toy, you can wipe out the database and re-initialize it, but obviously this isn’t an option for something that matters, and you just have to manually migrate the tables, based on the newly generated database schemas. Luckily the tables / columns are predictably named, but it still isn’t great.

*)
(* Note: Date is the date string used in the urls, as the most
   convenient serialization, Offset is seconds into the show *)
table u : {Token : string, Date : string, Offset : float} PRIMARY KEY Token
cookie c : string
(*

Ur/Web provides a mechanism to run certain code at times other than requests, called tasks. There are a couple categories, the simplest one being an initialization task, that is run once when the application starts up. We use this to initialize our random library.

*)
task initialize = fn () => Random.init
(*

Part of being a research project is that the standard libraries are pretty minimal, and one thing that is absent is date handling. You can format dates, add and subtract, and that’s about it. Since a bit of this application has to do with tracking what show is the current one, and whether you’ve already started watching it, I wrote a few functions to answer the couple date / time questions that I needed. These are all pure functions, and all the types are inferred.

*)
val date_format = "%Y-%m%d"

fun before_nine t =
    case read (timef "%H" t) of
        None => error <xml>Could not read Hour</xml>
      | Some h => h < 9
    
fun recent_show t =
   let val seconds_day = 24*60*60
       val nt = (if before_nine t then (addSeconds t (-seconds_day)) else t)
       val wd = timef "%u" nt in
   case wd of
       "6" => addSeconds nt (-seconds_day)
     | "7" => addSeconds nt (-(2*seconds_day))
     | _ => nt
   end
(*

The server that I have this application hosted on is in a different timezone than the show is broadcasted in (EST), so we have to adjust the current time so that we can tell if it is late enough in the day to get the current days broadcast. Depending on what timezone your computer is, this may need to be changed.

*)
fun est_now () =
    n <- now;
    return (addSeconds n (-(4*60*60)))

(*

We track users by tokens - these are short random strings generated with our random library. The mechanism for syncing devices is to visit the url (with the token) on every device, so the tokens will need to be typed in. For that reason, I didn’t want to make the tokens very long, which means that collisions are a real possibility. To deal with this, I set the length to be 6 characters, plus the number of tokens, log_26 (since users are encoded with lower case letters, n users can be encoded with log_26 characters, so we use this as a baseline, and add several so that the collision probability is low).

In this, we see how SQL queries work. You can embed SQL (a subset of SQL, defined in the manual), and this is translated into a query datatype, and there are many functions in the standard library to run those queries. We see here two: oneRowE1, which expects to get back just one row, and will extract 1 value from it. E means that it computes a single output expression. Note that it will error if there is no result, but since we are selecting the count, this should be fine. hasRows is an even simpler function; it simply runs the query and returns true iff there are rows.

Also note that we refer to the table by name as declared above, and we refer to columns as record members of the table. To embed regular Ur/Web values within SQL queries, we use {[value]}. These queries will not type check if you try to select columns that don’t exist, and of course does escaping etc.

*)
(* linking to cmath would be better, but since I only
   need an approximation, this is fine *)
fun log26_approx n c : int =
    if c < 26 then n else
    log26_approx (n+1) (c / 26)


(* Handlers for creating and persisting token *)
fun new_token () : transaction string =
    count <- oneRowE1 (SELECT COUNT( * ) FROM u);
    token <- Random.lower_str (6 + (log26_approx 0 count));
    used <- hasRows (SELECT * FROM u WHERE u.Token = {[token]});
    if used then new_token () else return token

(*

We write small functions to set and clear the tokens. We do this so that after a user has visited the unique player url at least once on each device, they will only have to remember the application url, not their unique url. now is a value of type transaction time, which gives the current time, and setCookie/clearCookie should be self explanatory.

*)
fun set_token token =
    t <- now;
    setCookie c {Value = token,
                 Expires = Some (addSeconds t (365*24*60*60)),
                 Secure = False}
    
fun clear_token () =
    clearCookie c

(*

The next thing is a bunch of html fragments. Ur/Web doesn’t have a “templating” system, but it is perfectly possible to create one by defining functions that take the values to insert in. I’ve opted for a simpler option, and just defined common pieces. HTML is written in normal XML format, within <xml> tags, and like the SQL tags, these are typechecked - having attributes that shouldn’t exist, nesting tags that don’t belong, or not closing tags all cause the code not to compile.

There are a couple rough edges - some tags are not defined (but you can define new ones in FFI modules), and some attributes can’t be used because they are keywords (hence typ instead of type), but overall it is a neat system, and works very well.

*)
fun heading () = 
    <xml>
        <meta name="viewport" content="width=device-width"/>
        <link rel="stylesheet" typ="text/css" href="http://dbpmail.net/css/default.css"/>
        <link rel="stylesheet" typ="text/css" href="http://lab.dbpmail.net/dn/main.css"/>
    </xml>

fun about () =
    <xml>
      <p>
      This is a player for the news program
      <a href="http://democracynow.org">Democracy Now!</a>
      that remembers how much you have watched.
    </p>
    </xml>
    
fun footer () =
    <xml>
      <p>Created by <a href="http://dbpmail.net">Daniel Patterson</a>.
        <br/>
        View the <a href="http://hub.darcs.net/dbp/dnplayer">Source</a>.</p>
    </xml>

(*

We now get to the web handlers. These are all url/form entry points, and do the bulk of the work. The first one, main, which we rewrote in dn.urp to be the root handler, is mostly HTML - the only catch being that if you have a cookie set, we just redirect you to the player.

getCookie returns an option CookieType where CookieType is the type of the cookie (in our case, it is a string). redirect takes a url, and urls can be created from handlers (ie, values of type transaction page) with the url function. So we apply player which is a handler we’ll define later, to the token value (as a token is the parameter that player expects), and grab a url for that.

One catch to this is that Ur/Web doesn’t know that player isn’t going to cause side effects, which would mean that it shouldn’t have a url created for it (side effecting things should only be POSTed to), which was why we had to declare player as safeGet in dn.urp

We also see a form that submits to create_player, which is another handler that we will define. One thing to note is that create_player is a unit -> transaction page function - and the action for the submit is just create_page, not create_page () - the action of submitting passes that parameter.

*)
fun main () =
    mc <- getCookie c;
    case mc of
        Some cv => redirect (url (player cv))
      | None => 
        return <xml>
          <head>
            {heading ()}
          </head>
          <body>
            <h2><a href="http://democracynow.org">Democracy Now!</a> Player</h2>
            {about ()}
            <p>
              You can listen to headlines on your way to work on your phone,
              pick up the first segment during lunch on your computer at work, and
              finish the show in the evening, without worrying what device you are
              on or whether you have time to watch the whole thing.
            </p>
            <h3>How it works</h3>
            <ol>
              <li>
                <form>
                  To start, if you've not created a player on any device:
                  <submit action={create_player} value="Create Player"/>
                </form>
              </li>
              <li>Otherwise, visit the url for the player you created (it should look like
                something <code>http://.../player/hcegaoe</code>) on this device
                to synchronize your devices. You only need to do this once per device, after that
                just visit the home page and we'll load your player.
              </li>
            </ol>

            <h3>Compatibility</h3>
            <p>This currently works with Chrome (on computers and Android) and iPhones/iPads.</p>  
            {footer ()}
          </body>
        </xml>

(*

create_player is pretty straightforward, but it shows a different part of Ur/Web’s SQL support: dml supports INSERT, UPDATE, and DELETE, in the normal ways, with the same embedding as SQL queries (that {[value]} puts a normal Ur/Web value into SQL). We create a token, create a “user”, setting that they are on the current day’s show and at the beginning of it (offset 0.0), store the token, and then redirect to the player.

*)
and create_player () =
    n <- est_now ();
    token <- new_token ();
    dml (INSERT INTO u (Token, Date, Offset)
         VALUES ({[token]}, {[timef date_format (recent_show n)]}, 0.0));
    set_token token;
    redirect (url (player token))

(*

The next two functions encompass most of the player, which is the core of the application. The way that it is structured is a little odd, but with justification: Chrome on Android caches extremely aggressively, and doesn’t seem to pay attention to headers that say not to, which means that if you visited the application, and then a few days later open up Chrome again, it will seem like it is loading the page, but it is loading the cached HTML, it is not getting it from the server. This is really bad for us, because it means it will have both an old offset (in case you watched some of the show from another device), but worse, on subsequent days it will be trying to play the wrong day’s show! You can manually reload the page, but this is silly, so what we do is initially just load a blank page, and then immediately make a remote call to actually load the page. So what is cached is a little bit of HTML and some javascript that loads the page for real.

We do all of this is functional reactive style: we declare a source, which is a place where values will be put, and it will cause parts of the page (that are signaled) to update their values. Then we set an onload handler for the body, which, first, makes an rpc call to a server side function (which is just another function, like all of these handlers), and then set the source that we defined to be the result of rendering the player. render is a client-side function that just creates the appropriate forms / html.

Finally, we will call a client-side function init, which will do some setup and then call into the javascipt ffi to the ffi init function, which will handle the HTML5 audio/video APIs (which Ur/Web doesn’t support, and are very browser specific anyway).

One incredibly special thing that is going on is the SourceL.set os that is passed to javascript. If you remember from our .urp file, we imported sourceL. It is a special reactive construct that allows you to set up handlers that cause side effects (are transactions) when the value inside the SourceL changes. So what is happening is we have created one of these on the server, in player_remote, and sent it back to the client. The client then curries the set function with that source, producing a single argument function that just takes the value to be updated. We hand this function to javascript, so that in our FFI code, we can just set values into this, and it can reactively cause stuff to happen in our server-side code.

The reactive component on the page is the <dyn> tag, which is a special construct that allows side-effect free operations on sources. signal s grabs the current value from the source s, and in this case we just return this, but we could do various things to it. The result of the block is what the value of the <dyn> tag is. In this case, we have just made a place where we can stick HTML, by calling set s some_html.

*)
and player token =
    s <- source <xml/>;
    return <xml>
      <head>
        {heading ()}
      </head>
      <body onload={v <- rpc (player_remote token);
                    set s (render token v.Player v.Show);
                    init token v.Player v.Source (SourceL.set v.Source) v.Video v.Audio}>
        <dyn signal={v <- signal s; return v}/>
      </body>
      </xml>
(*

The remote component is where most of the logic of the player resides. By now, you should be able to read most of what’s going in. Some points to highlight are the place where we create the SourceL that we will pass back, and set its initial value to offset. Also, fresh is a way of generating identifiers to use within html. Our render function will use this identifier for the player, which is necessary for the javascript FFI to know where it is. Finally, bless is a function that will turn strings into urls, by checking against the policy outlined in the .urp file for the application.

*)
and player_remote token =
    n <- est_now ();
    op <- oneOrNoRows1 (SELECT * FROM u WHERE (u.Token = {[token]}));
    case op of
        None =>
        clear_token ();
        redirect (url (main ()))
      | Some pi =>
        set_token token;
        let val show = recent_show n
            val fmtted_date = (timef date_format show) in
            (if fmtted_date <> pi.Date then
                (* Need to switch to new day *)
                dml (UPDATE u SET Date = {[fmtted_date]}, Offset = 0.0 WHERE Token = {[token]})
            else
                return ());
            let val offset = (if fmtted_date = pi.Date then pi.Offset else 0.0)
                val video_url = bless (strcat "http://dncdn.dvlabs.com/ipod/dn"
                                              (strcat fmtted_date ".mp4"))
                val audio_url = bless (strcat "http://traffic.libsyn.com/democracynow/dn"
                                              (strcat fmtted_date "-1.mp3")) in
            os <- SourceL.create offset;
            player_id <- fresh;
            
            return {Player = player_id, Show = show, Offset = offset,
                    Source = os, Video = video_url, Audio = audio_url}
            end
        end


(*

The next three functions are simple - the first just renders the actual player. Note that we use the player_id we generated in player_remote. Then we provide a way to forget the player (if you want to unlink two devices, forget the player on one and create a new one), and due to some imperfections with how we keep the time in sync (mostly based on weirdness of different browsers implementations of the HTML5 video/audio APIs), to seek backwards, or start the show over, we need to tell the server explicitly, so we provide a handler to do that.

*)
and render token player_id date =
     <xml><h2>
       <a href="http://democracynow.org">Democracy Now!</a> Player</h2>
       {about ()}
       <h3>{[timef "%A, %B %e, %Y" date]}</h3>
       <div id={player_id}></div>
       <br/><br/><br/>
       <form>
         <submit action={start_over token} value="Start Show Over"/>
       </form>
       <form>
         <submit action={forget} value="Forget This Device"/>
       </form>
       {footer ()}
     </xml>

(* Drop the cookie, so that client will not auto-redirect to player *)
and forget () =
    clear_token ();
    redirect (url (main ()))

(* Because of browser quirks, this is the only way to get to an earlier time, synchronized *)
and start_over token () =
    dml (UPDATE u SET Offset = 0.0 WHERE Token = {[token]});
    redirect (url (player token))

(*

Now we get to the last web handlers. The first one is a client side initializer. The main thing it sets up is a handler to rpc to the server whenever the offset SourceL changes. The call is to update (which we’ll define in a moment), and it optionally returns a new time to set the client to.

This may sound a little odd, but the basic situation is that you play part of the way through the show on one device, then pause, watch some on another device, and now hit play on the first device. It will POST a new time, but the server will tell it that it should actually be at a later time, and so we use the javascript FFI function set_offset to set the offset.

Finally we make it so that the client silently fails if the connection fails (this is bad behavior, but simple), and call the javascript FFI initialization function, which will set up the player and any HTML5 API related stuff.

*)
and init token player_id os set_offset video_url audio_url =
    SourceL.onChange os (fn offset => newt <- rpc (update token offset);
                                      case newt of
                                          None => return ()
                                        | Some time => Dnjs.set_offset time);
    offset <- SourceL.get os;
    onConnectFail (return ());
    Dnjs.init player_id offset set_offset video_url audio_url

(*

The last function is the simple handler that we called when the offset SourceL changes. It updates the time if the time is greater than the recorded offset (this is why we need the start_over handler), and otherwise returns the recorded offset to be updated.

*)
and update token offset =
    op <- oneOrNoRows1 (SELECT * FROM u WHERE (u.Token = {[token]}));
    case op of
         None => return None
       | Some r => (if offset > r.Offset then 
                       dml (UPDATE u SET Offset = {[offset]}
                            WHERE Token = {[token]} AND {[offset]} > Offset);
                       return None
                   else return (Some r.Offset))

sourceL.urs

(*

This came from a supplemental standard library, and, as explained earlier, allows you to create source-like containers that call side-effecting handlers when their values change.

*)
(* Reactive sources that accept change listeners *)

con t :: Type -> Type

val create : a ::: Type -> a -> transaction (t a)

val onChange : a ::: Type -> t a -> (a -> transaction {}) -> transaction {}

val set : a ::: Type -> t a -> a -> transaction {}
val get : a ::: Type -> t a -> transaction a
val value : a ::: Type -> t a -> signal a

sourceL.ur

(*

The sourceLs are built on top of normal sources, and just call the OnSet function when you call set.

*)

con t a = {Source : source a,
           OnSet : source (a -> transaction {})}

fun create [a] (i : a) =
    s <- source i;
    f <- source (fn _ => return ());

    return {Source = s,
            OnSet = f}

fun onChange [a] (t : t a) f =
    old <- get t.OnSet;
    set t.OnSet (fn x => (old x; f x))

fun set [a] (t : t a) (v : a) =
    Basis.set t.Source v;
    f <- get t.OnSet;
    f v

fun get [a] (t : t a) = Basis.get t.Source

fun value [a] (t : t a) = signal t.Source

dnjs.urs

(*

This is the signature file for our javascript FFI. It declares what functions will be exported to be accessible within Ur/Web, and what types they have.

*)
val init : id -> (* id for player container *)
           float -> (* offset value *)
           (float -> transaction unit) -> (* set function *)
           url -> (* video url *)
           url -> (* audio url *)
           transaction unit

val set_offset : float -> transaction unit

dn.js

/*

Since this is a adventure in Ur/Web, not Javascript, and there are plenty of places to learn about the quirks and features of HTML5 media APIs (and I don’t claim to be an expert), I’m just going to paste the code in without detailed commentary. The only points that are worth looking at are how we use setter, which you will remember is a curried function that will be updating a SourceL, causing rpcs to update the time. To call functions from the FFI, you use execF, and to force a transaction to actually occur, you have to apply the function (to anything), so we end up with double applications.

Other than that, all that is here is some browser detection (as different browsers have different media behavior) and preferences about media type in localstorage.

*/
function init(player, offset, setter, video_url, audio_url) {
    // set up toggle functionality
    $("#"+player).after("<button id='toggle'>Switch to " +
                        (prefersVideo() ? "audio" : "video") + "</button>");
    $("#toggle").click(function () {
        window.localStorage["dn-prefers-video"] = !prefersVideo();
        location.reload();
    });

    // put player on the page
    if (canPlayVideo() && prefersVideo()) {
        $("#"+player).html("<video id='player' width='320' height='180' controls src='" +
                           video_url + "'></video>");
    } else {
        $("#"+player).html("<audio id='player' width='320' controls src='" +
                           audio_url + "'></audio>");
    }

    // seek / start the player, if applicable
    if (isDesktopChrome()) {
        $("#player").one("canplay", function () {
            var player = this;
            if (offset != 0) {
                player.currentTime = offset;
            }
            player.play();
            window.setInterval(update_time(setter), 1000);
        });
    } else if (isiOS() || isAndroidChrome()) {
        // iOS doesn't let you seek till much later... and won't let you start automatically,
        // so calling play() is pointless
	$("#player").one("canplaythrough",function () {
	    $("#player").one("progress", function () {
		if (offset != 0) {
                    $("#player")[0].currentTime = offset;
                }
                window.setInterval(update_time(setter), 1000);
	    });
	});   
    } else {
        $("#player").after("<h3>As of now, the player does not support your browser.</h3>");
    }
}

function set_offset(time) {
    var player = $("#player")[0];
    if (time > player.currentTime) {
        player.currentTime = time;
    }
    
}

// the function that grabs the time and updates it, if needed
function update_time(setter) {
    return function () {
        var player = $("#player")[0];
        if (!player.paused) {
            // a transaction is a function from unit to value, hence the extra call
            execF(execF(setter, player.currentTime), null)
        }
    };
}

// browser detection / preference storage

function canPlayVideo() {
    var v = document.createElement('video');
    return (v.canPlayType && v.canPlayType('video/mp4').replace(/no/, ''));
}

function prefersVideo() {
    return (!window.localStorage["dn-prefers-video"] || window.localStorage["dn-prefers-video"] == "true");
}

function isiOS() {
    var ua = navigator.userAgent.toLowerCase();
    return (ua.match(/(ipad|iphone|ipod)/) !== null);
}

function isDesktopChrome () {
    var ua = navigator.userAgent.toLowerCase();
    return (ua.match(/chrome/) !== null) && (ua.match(/mobile/) == null);
}

function isAndroidChrome () {
    var ua = navigator.userAgent.toLowerCase();
    return (ua.match(/chrome/) !== null) && (ua.match(/android/) !== null);
}

Makefile

To actually build our application, we have to first build our C library. Then we’ll build the app, using the sqlite backend. To get this running, we then need to do sqlite3 dn.db < dn.sql (note you only need to do this once) and then start the server with ./dn.exe. You can then visit the application at http://localhost:8080. This has been tested on current Debian Linux and Mac OSX.

all: app

librandom.a: librandom.o
	ar rcs [email protected] $<

librandom.o: random.c
	gcc -I/usr/local/include/urweb -g -c -o [email protected] $<

app: dn.ur dn.urs dn.urp librandom.a
	urweb -dbms sqlite -db dn.db dn

.PHONY: clean

clean:
	rm -f librandom.a librandom.o