Animation Using the Netscape Browser

Dynamic documents via server push and client pull

Andrew Davison

Andrew is a lecturer in the Department of Computer Science at the University of Melbourne, Australia. He can be reached at ad@cs.mu.oz.au.


One way of sending animation to a Web page is to include a link to a piece of video. This could require an expensive investment in hardware and software for manipulating the image. Also, video files are often very large, which can be a problem for users accessing them over a network. Aside from these concerns, many animation effects really don't need video technology. For instance, a great deal can be achieved by rapidly displaying a sequence of GIF files.

GIFs do not require special equipment to be displayed. Indeed, every graphical browser can treat them as inline images. There is a plethora of software for manipulating GIFs, and extensive libraries of clip art (see http://www.yahoo.com/ Computers/Multimedia/Pictures/ Clip_Art/).

Netscape 1.1 (and later) can display sequences of GIF files. In fact, its "client-pull" and "server-push" dynamic-document capabilities permit a variety of animation effects.

Client Pull

Client pull makes it possible for a client (the user's Netscape browser) to request a new page without the intervention of the user. This is achieved through a META tag (see Example 1) in the head of the HTML document being processed by the browser. The Content attribute specifies the delay in seconds before the new page is requested, and the URL attribute identifies the location of that page. The URL must be fully specified; relative addresses are insufficient.

You can use this technique to animate the introduction to a page. The animation effect is heightened by using the same layout in all the introductory HTML files. Examples 2(a), 2(b), and 2(c) are three HTML files (intro1.html, intro2.html, and intro3.html, respectively) that generate Figures 1(a), 1(b), and 1(c). Together they form a three-stage introduction to a questionnaire in quest2.html (see Figure 2). The relevant METAtags from each intro file are shown in Example 3. The METAtag in intro1.html causes it to be replaced after one second by intro2.html. The META tag in intro2.html causes it to be replaced after another second by intro3.html. The META tag in intro3.html causes it to be replaced after one more second by quest2.html. The questionnaire does not contain a META tag, thereby terminating the rapid change of pages. Try the animation (and the questionnaire) for yourself by accessing http://www.cs.mu.oz .au/~ad/code/intro1.html

If the URL attribute is left out of the META tag, then the page itself will be reloaded after the specified delay. The file coffee.html (see Example 4 and Figure 3, or http://www .cs.mu.oz.au/~ad/code/ quest/coffee.html) is an example of a page of this type. It is loaded every 60 seconds until the browser terminates or is set to point to another page. The displayed GIF file can be generated by a camera monitoring something interesting (the office coffee maker, for example) and left running. The browser will load the current coffeepot picture as it reloads coffee.html.

An advantage of this approach is that only one GIF file needs to be stored, thereby reducing the memory requirements. A disadvantage is that the browser has to continually reload the page, which means multiple connections to the server.

Remember that the Content-attribute value is only approximate, since it does not take into account the time to establish a server connection, retrieve the file, and display it. Therefore, the period between reloads is typically longer than the Content value suggests, and the extra delay depends on variables such as network and machine usage.

Server Push

A server-push-based dynamic document is sent to the client (the user's browser) by the server. Such a document uses an experimental, multipart MIME type called "multipart/x-mixed-replace," which allows its data items to be treated as separate documents by the receiving browser.

Figure 4 presents the general format of a multipart/x-mixed-replace document: data1, data2,..., dataN are treated as separate documents by the browser. The arrival of a new piece of data (document) will cause the previous one to be cleared from the browser's window and the new one to be displayed. SomeString can be any string, but it must be preceded by "-" when used as a data separator. When used as a terminator for the document, it must also be followed by "-". When a browser receives a data separator (or terminator), any data that is still in the browser's buffers will be displayed.

The data can be any recognized MIME type, including HTML, plaintext, or GIF. The data must begin with a Content-Type declaration, followed by a blank line and the actual information. The Content-Type values for HTML, plaintext, and GIF data are text/html, text/plain, and image/gif, respectively. (A complete list of official MIME types can be found at ftp://ftp.isi.edu/in-notes/iana/assignments/media-types/ media-types.)

Typically, multipart documents are constructed by CGI scripts and utilize a delaying mechanism between the transmission of each piece of data. This gives the user time to read the data before it is replaced. CGI scripts can also create multipart documents consisting of an infinite number of data items (for instance, by outputting data from a loop). This is useful if the application requires a continual stream of information to be sent to the client (the periodic results of monitoring a network, for example).

Listing One is simp-serv.c, a program that demonstrates how a simple, multipart document can be constructed using the CGI approach. It is invoked by the HTML document in Example 5 (also at http://www.cs .mu.oz.au/~ad/code/quest/converse.html). When simp-serv.c is called, it transmits a multipart document consisting of two data items corresponding to two HTML pages. The first printf("-HTMLSep") statement makes sure that the multipart Content-Type string has been output to the browser. Then the call to sleep() suspends the output of the first piece of data for one second. The second printf("-HTMLSep") statement makes sure that all of the first data item (the first HTML page, in this case) has been output. The second sleep() call suspends the output of the second data item (the second HTML page) for three seconds, allowing time for the user to read the first page before it is replaced.

Server push differs from client pull in a number of ways. The most important one is that the server sends a stream of data items to the client using one, possibly long-lived, network connection. In contrast, client pull uses a series of separate requests over the network to obtain its documents. Thus, server push is more efficient since it only needs to set up one connection; however, it may monopolize a TCP/IP port for a lengthy period. Server push is easier to control than client pull: It can be terminated by the user pressing the browser's Stop button or by the server terminating the multipart document.

Server Push with Images

You can generate animation effects by using server push to send a sequence of GIF images to the browser. Consequently, the multipart document will consist of data items whose Content-Type is image/gif.

If the retrieval of the multipart document is initiated from inside an IMG tag in an HTML document, then the sequence of images will appear at that spot without altering the rest of the document. Also, the dimensions of the first image will be used to scale all the subsequent images to the same size.

Rather than writing a GIF-filled multipart document directly, I'll use frames.c, a CGI script, to generate it (see Listing Two). The script must be passed the name of a file containing a list of GIF filenames that are to be sent to the browser.

Example 6 shows how frames.c is utilized. The important part is the IMG tag <img src="http://www.cs.mu.oz.au/cgi-bin/frames?intro">, which causes the compiled frames.c code to be invoked with the text after the "?" as its command-line argument. frames.c interprets it as the file intro.pics in the directory /home/staff/ad/ www_public/code/quest/gifs.

The .pics file format is simple, consisting of lines of filenames, optionally preceded by integer delay values. Example 7, for instance, shows the contents of intro.pics, which is interpreted as a request to immediately transmit hello.gif to the browser. frames.c waits for two seconds before sending blank.gif and finally transmits prepare.gif. Then it will wait another two seconds before sending blank .gif and finish by transmitting question.gif. frames.c assumes that all of these GIF files are in /home/staff/ad/www_public/code/ quests/gifs.

The processing of intro.pics is similar to the example in the client-pull section, but the images all appear in the same Web page. Using this approach, the questionnaire file, quest2.html, could be modified to include the same IMG tag at its start, making the animated introduction part of the questionnaire.

A second .pics file is shown as Example 8. When read by an IMG tag, it sends a stream of different images to the browser. The new feature illustrated by this example is the "times" line at the end, which specifies that the sequence of images will be sent ten times. (You can find the images used in Example 7 and Example 8 at http:// www.cs.mu.oz.au/~ad/code/ quest/gifs.)

Inside frames.c

frames.c first adds the path and .pics extension to its command-line argument. Then the file is opened, and build_pics() reads the delay and GIF-filename information into the pics array. build_pics() checks for the presence of a times line at the end of the pics array by calling num_ times(). num_ times() sets tmsno to the value on the times line, or to 0 if there is no such line.

The multipart Content-Type is written to standard output (and thus, to the browser), and a Do-While loop is entered that continues until tmsno is equal to (or less than) 0. The nested For loop processes pics' elements, each of which corresponds to a GIF file and a delay value (if no delay was given, then this is 0). The separator string is printed first, ensuring that the previous image is fully displayed, and then the pics element is processed. Any delay is handled by calling sleep(), then the GIF is output using write_gif().

write_gif() and wstring() are derived from code written by Rob McCool, which can be found at ttp://home.netscape.com/ assist/net_sites/mozilla/doit.c. write_gif() does not read the GIF file into a data structure prior to writing it to standard output. Instead, the file is memory mapped to a process address, which is more efficient, especially if the file is large. However, this technique uses the low-level file operations open() and write(), which utilize integer file descriptors, while the standard I/O library functions use streams. The two mechanisms do not work together, so the rest of the output from the program must also use write(). This explains the use of wstring() to print a string, rather than printf(). Another drawback is that the memory-mapping library is not included with every flavor of UNIX. For this reason, a version that uses read() is included as write_gifp(). Change the write_gif() call in main() to write_gifp() if memory mapping is unavailable.

Server-Push Issues

Delays specified by the server (by using sleep(), for example) only delay the transmission of the data. They do not directly influence when the client browser will receive, load, and display the data. For instance, the first few pieces of data (GIF images, HTML pages, or whatever) sent to a browser often take some time to be loaded and displayed. As a result, any server-side delays between these data items will be less apparent to the browser user.

Also, the browser can decide to stop displaying data if too much is waiting at the browser end of the network connection. This may happen if many pieces of data are sent without server-side delays between them.

Summary

Animation using video can be costly and complicated, and the same effects can often be achieved by displaying a sequence of GIF images or HTML pages. Netscape (1.1 and later) can be used to code this type of animation, using client-pull and server-push dynamic documents.

Client pull enables HTML pages to be automatically loaded after a given time. This makes it straightforward to build introductory animation sequences and pages that regularly reload themselves.

Server push can be used to send a sequence of data to a client browser. The program I've described makes server push easier to use. Delays can be specified between the images, and the sequence can be repeated an arbitrary number of times.

Further details on dynamic documents can be found at the Netscape site (http:// www.netscape.com/assist/net_sites/dynamic_docs.html) and at the Animationest page at http://bakmes.colorado.edu/~bicanic/altindex.html. Invented World's test page, at http://www.enterprise.net/iw/testpage.html, has several interesting server-push examples, including animated lightning, blooming roses, and Jupiter in motion, all created with Invented World's Webvid '95 program. Animate v0.9, written in Perl, can be found at http://www.homepages.com, together with some nice examples.

For details on other Netscape extensions to HTML, see http://www.netscape.com/assist/net_sites/html_extensions.html.

Example 1: A META tag in the head of the HTML document
<meta http-equiv="Refresh" content="5;
url=http://www.cs.mu.oz.au/~ad/code/quest/quest.html">

Example 2: The three HTML files that generate Figures 1(a), 1(b), and 1(c).

(a) intro1.html; (b) intro2.html; (c) intro3.html. (a) intro1.html <html> <head> <meta http-equiv="Refresh" content="1; url=http://www.cs.mu.oz.au/~ad/code/quest/intro2.html"> <title>A Rather Silly Computing Questionnaire</title> </head> <h1>A Rather Silly Computing Questionnaire</h1> <br> <img src="gifs/hello.gif" alt="Hello... "> </body> </html> (b) intro2.html <html> <head> <meta http-equiv="Refresh" content="1; url=http://www.cs.mu.oz.au/~ad/code/quest/intro3.html"> <title>A Rather Silly Computing Questionnaire</title> </head> <h1>A Rather Silly Computing Questionnaire</h1> <br> <img src="gifs/prepare.gif" alt="prepare to... "> </body> </html> (c) intro3.html <html> <head> <meta http-equiv="Refresh" content="1; url=http://www.cs.mu.oz.au/~ad/code/quest/quest2.html"> <title>A Rather Silly Computing Questionnaire</title> </head> <h1>A Rather Silly Computing Questionnaire</h1> <br> <img src="gifs/question.gif" alt="fill in the questionnaire... "> </body> </html>

Example 3: (a) The META tag in intro1.html causes a one-second delay between Figures 1(a) and 1(b); (b) another one-second delay is caused before Figure 1(c) appears; (c) intro3.html requests the questionnaire in Figure 2 after another second, as defined by its META tag. (a) <meta http-equiv="Refresh" content="1; url=http://www.cs.mu.oz.au/~ad/code/quest/intro2.html"> (b) <meta http-equiv="Refresh" content="1; url=http://www.cs.mu.oz.au/~ad/code/quest/intro3.html"> (c) <meta http-equiv="Refresh" content="1; url=http://www.cs.mu.oz.au/~ad/code/quest/quest2.html"> Example 4: The file coffee.html. <html> <head> <meta http-equiv="Refresh" content=60> <title>How's the Coffee?</title> </head> <body> <h1>How's the Coffee?</h1> The coffee pot:<p> <img src="gifs/coffee.gif" alt="coffee gif unavailable"> </body> </html>

Example 5: This HTML file invokes simp-serv.c (Listing One). <html> <head> <title>A Simple Conversation</title> </head> <body> <h1>A Simple Conversation</h1> <br> Start a conversation <a href="http://www.cs.mu.oz.au/

cgi-bin/simp-serv">now</a>.<p> </body> </html>

Example 6: How frames.c (Listing Two) is utilized. <html> <head> <title>Server Push Animation</title> </head> <h1>Server Push Animation</h1> <br> <img src="http://www.cs.mu.oz.au/

cgi-bin/frames?intro"> </body> </html>

Example 7: Contents of intro.pics. # introductory images for the

# questionnaire hello 2 blank prepare 2 blank question

Example 8: A typical .pics file. # A crazy mishmash of images 2 chitz cloudz 2 cracks fire galaxy4 2 marble hello times 10

Figure 1: (a) First image in the animation sequence; (b) second image in the sequence; (c) final introductory image in the sequence.

Figure 2: The questionnaire.

Figure 3: Watching the coffeepot

Figure 4: General format of a multipart/x-mixed-replace document Content-type: multipart/x-mixed-

replace;boundary=SomeString -SomeString data1 -SomeString data2 -SomeString data3 . . . -SomeString dataN -SomeString-

Listing One

/* simp-serv.c  */
/* By Andrew Davison (ad@cs.mu.oz.au), August 1995 */
/* A simple server push program that sends two HTML
   pages to the browser with some delays. */
#include <stdio.h>
#include <unistd.h>    /* for sleep() */
void print_html(char *title, char *body);
int main()
{
  printf("Content-type: multipart/x-mixed-replace;boundary=HTMLSep\n");
  printf("\n--HTMLSep\n"); 
  sleep(1);
  print_html("Hello", "Hello User!");
  printf("\n--HTMLSep\n"); 
  sleep(8); 
  print_html("Goodbye", "Goodbye User!");
  printf("\n--HTMLSep--\n");
  return 0;
}
void print_html(char *title, char *body)
/* Print out the title and body strings in HTML document format */
{
  printf("Content-type: text/html\n\n");
  printf("<html><head>\n");
  printf("<title>%s</title>\n", title);
  printf("</head><body>\n");
  printf("<h1>%s</h1>\n", title);
  printf("%s<p>\n", body);
  printf("</body></html>\n");
}

Listing Two

/* frames.c  -- by Andrew Davison (ad@cs.mu.oz.au), August 1995 */
/* Read a pics filename from the command line and load
   the contents into a pics array.
   The pics array is used to send a sequence of gifs
   to standard output (with optional delays) as a 
   MIME multipart/mixed message.
   The intention is to display the gifs in 
   netscape v1.1 (or later) as an animation.
   The gifs sequence can be repeatedly sent, depending
   on the presence of "times tmsno" in the pics file
   (tmsno is the number of times the sequence should be
   sent to the browser.
*/
/* The location of the gifs and pics files is hardwired
   into the program as the PATH constant.
*/
/* write_gif() and wstring() are
   based on code by Rob McCool, available at
   http://home.netscape.com/assist/net_sites/mozilla/doit.c
*/
#include <sys/types.h>
#include <sys/mman.h>      /* for mmap(), munmap() */
#include <unistd.h>        /* for sleep() */
#include <fcntl.h>         /* for open(), write() */
#include <sys/stat.h>      /* for fstat() */
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#define PATH "/home/staff/ad/www_public/code/quest/gifs"
         /* path to the gifs and pics file; change to suit */
#define NAME 20       /* max chars in a number string */
#define MAXLEN 120    /* max length of a filename/line */
#define MAXPICS 50    /* max no. of pictures */
#define BUFSIZE 512   /* size of chunk to be read from gif file */
typedef struct {
  int delay;          /* delay before gif is sent */
  char *name;         /* gif filename */
} frame;
int build_pics(FILE *fp, frame pics[], int *pnum);
int extract_delay(char *ln, int *ppos);
int num_times(char *s);
void write_gif(int fd);
void wstring(char *s);
void write_gifp(int fd);  
int main(int argc, char *argv[])
{
  FILE *fp;
  int filedes;
  frame pics[MAXPICS];       /* array of delays & gif filenames */
  char fnm[MAXLEN];          /* full pics filename */
  char gif_name[MAXLEN];     /* full gif filename */
  int picnum, x, tmsno;
  sprintf(fnm, "%s/%s.pics", PATH, argv[1]);  /* build full pics fnm */
  if ((fp = fopen(fnm, "r")) == NULL)
    exit(0);
  else {
    tmsno = build_pics(fp, pics, &picnum);
    fclose(fp);
  }
  wstring("Content-type: multipart/x-mixed-replace;boundary=GifSeperator\n");
  do {     /* always do actions at least once */
    for (x=0; x < picnum; x++) {
      wstring("\n--GifSeperator\n");
      sleep(pics[x].delay);
      wstring("Content-type: image/gif\n\n");
  
      sprintf(gif_name, "%s/%s.gif", PATH, pics[x].name);
                              /* build full gif filename */
        
      if((filedes = open(gif_name, O_RDONLY)) != -1) {
        write_gif(filedes);       /* or write_gifp(filedes); */
        close(filedes);
      }
    }
    tmsno--;
  } while (tmsno > 0);
  wstring("\n--GifSeperator--\n"); 
  return 0;
}
int build_pics(FILE *fp, frame pics[], int *pnum)
/* Read in lines from the pics file using the fp file pointer
   and store the delays and gif filenames in the pics array.
   Lines starting with a '#' are comments and are ignored.
   Lines beginning with a new line are also ignored.
   The last pics entry is checked by num_times() to see if
   it contains "times tmsno". If it does then this entry
   is ignored but tmsno is recorded.
*/
{
  char line[MAXLEN];
  int num, len, letpos, tmsno;
  num = 0;
  while ((fgets(line, MAXLEN, fp) != NULL) && (num < MAXPICS))
    if ((line[0] != '\n') && (line[0] != '#'))
    {             /* not a blank line or comment*/
      len = strlen(line);
      if (line[len-1] == '\n')
        line[--len] = '\0';     /* overwrite '\n'; decr len */
      if (isdigit(line[0]) != 0) {
        pics[num].delay = extract_delay(line, &letpos);
        pics[num].name = (char *)malloc(sizeof(char)*(len-letpos+1));
        strcpy(pics[num].name, &line[letpos]);
      }
      else {
        pics[num].delay = 0;
        pics[num].name = (char *)malloc(sizeof(char)*(len+1));
        strcpy(pics[num].name, line);
      }
      num++;
    }
  if ((tmsno = num_times(pics[num-1].name)) > 0)
    num--;        /* ignore the last pics array entry */
  *pnum = num;
  return tmsno;
}
int extract_delay(char *ln, int *ppos)
/* Attempt to extract a number from the start of the ln line.
   Also find the position of the first non-white space 
   character and store it in ppos. The gif filename begins
   at that position.
*/
{
  int i = 0;
  char num[NAME];
  while (isdigit(ln[i]) != 0) {
    num[i] = ln[i];
    i++;
  }
  num[i] = '\0';
  while (isspace(ln[i]) != 0)
    i++;
  *ppos = i;
  return atoi(num);
}
int num_times(char *s)
/* If the s line begins with "times" then extract
   the number following it, skipping any white space
   in between.
   If there isn't a "times" string then return 0, so allowing
   build_pics() to detect the string's absence. 
   If there is a "times" string but no number then return 1.
*/
{
  int pos = 5;   /* length of "times" */
  int tmsno = 0;
  if (strncmp(s,"times",pos) == 0) {
    while((s[pos] != '\0') &&
          (isspace(s[pos]) != 0))
      pos++;
    if (s[pos] == '\0')
      tmsno = 1;     /* a "times" string with no number */
    else
      tmsno = abs(atoi(&s[pos]));   /* avoid -ve */
  }
  return tmsno;
}
/* Functions based on code by Rob McCool */
void write_gif(int fd)
/* Use memory mapping instead of read() to access the
   file with the fd file descriptor.
*/
{
  struct stat fi;     /* file information */
  caddr_t pa;         /* process address */
  fstat(fd, &fi);
  pa = mmap(NULL, fi.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
  if(pa == (caddr_t) -1)
    exit(0);
  if(write(STDOUT_FILENO, (void *) pa, fi.st_size) == -1)
    exit(0);
  munmap(pa, fi.st_size);
}
void wstring(char *s)
/* Write the s string to stdout */
{
  if (write(STDOUT_FILENO, s, strlen(s)) == -1)
    exit(0);
}
/* A more portable version of write_gif() */
void write_gifp(int fd)
/* Use read() to access the file with the fd file descriptor. */
{
  char buffer[BUFSIZE];
  int nread;
  while ((nread = read(fd, buffer, BUFSIZE)) > 0)
    if (write(STDOUT_FILENO, buffer, nread) == -1)
      exit(0);
}

Copyright © 1995, Dr. Dobb's Journal