Writing a Multimedia App in Liana

Jack describes a large multimedia application he wrote using the Liana programming system.


January 01, 1994
URL:http://www.drdobbs.com/tools/writing-a-multimedia-app-in-liana/184409382

Figure 1


Copyright © 1994, Dr. Dobb's Journal

Figure 1


Copyright © 1994, Dr. Dobb's Journal

SP 94: Writing a Multimedia App in Liana

Writing a Multimedia App in Liana

Dynamic behavior requires a dynamic language

Jack Krupansky

Jack is president of Base Technology, where he develops and markets Liana and consults on development of Windows applications. He can be reached at 303-440-4558, by fax at 303-444-4186, on CompuServe at 70642,2662, or at 1320 Pearl St., Suite 110-B, Boulder, CO 80302.


Three fashionable topics that get a lot of talk are multimedia, Windows, and object-oriented programming (OOP). But sometimes it's hard to find instances in which these technologies are applied to real-world problems. In this article, I'll talk about a multimedia application that's currently in day-to-day use at a number of corporations and describe how it was developed.

I developed Avenue as a consulting project for VIS Development Corp. (Waltham, MA). In implementing Avenue, I used the Liana object-oriented programming language and class library. Liana is a development tool I created that lets you build small to medium-size Windows applications very quickly. (See Ray Valdés's "The Liana Programming Language," DDJ, October 1993.)

Enhancing Training Videos

Although businesses have much money invested in training videos, managers may wonder if these videos are effective in imparting knowledge, given the passive nature of video-watching. To make the learning experience more interactive, VIS Development offers a service that digitizes existing training video tapes, places them on CD-ROM, and structures them via an interactive graphical front end: Avenue. The result is a package that is more engaging to users. The package can also gather statistics on usage, as well as conduct tests on comprehension.

Last year, VIS staff prototyped Avenue using a DOS-based multimedia authoring system. Then they decided to move it to Windows using Asymetrix ToolBook. However, progress soon stalled because the staff lacked programming experience. When I was brought on board to implement the application, I proposed Liana as a way of obtaining a quality result as soon as possible. Liana's interpretive nature and high level of abstraction free you from much of the details of C/C++ programming and the Windows API. Although Liana did not have any multimedia support, a quick review of the Media Control Interface (MCI) led me to the conclusion that a Liana video class would be very easy to implement. I was able to get a preliminary version of the class running within a few hours. But before discussing my Liana implementation, I'll describe Avenue further.

Overview of Avenue

Avenue allows the user to walk up to a PC and quickly select and start a video training course. The hierarchical structure of the front end allows the user to visually navigate to the desired training video; see Figure 1.

Avenue is really a meta-application, akin to an authoring tool. It adds menus, which can be full-screen bitmap images, and buttons, which can be of arbitrary size. It operates via keyboard or mouse, but is also well suited for use with touch screens. The images may be as simple as text menus or as sophisticated as scanned photographs or diagrams that allow users to visually select an item of interest. A selected item might immediately bring up a training-video segment, or, alternately, a more-detailed image might be shown so that the user can make a more precise selection. The author of the training application can also specify an initial logon dialog, so that the material can be tailored to each user's skill level. Progress through the video can also be a result of passing tests in previous training sessions.

Course authors, managers, and system administrators can customize a particular training video via a set of scripts and configuration files. In addition to controlling the order of presentation of video segments, the scripts define the hierarchical structure of the presentation and can fine-tune the appearance of the screen.

The users of Avenue-produced training materials vary widely in skill level, depending on the course material; they can include factory workers and sales personnel, as well as corporate managers. Some users are comfortable with a dense set of choices and controls; others will want a friendlier, sparser presentation of choices with fewer controls. Avenue can accommodate changes without requiring expensive video editing or custom software development.

Avenue has a facility for logging who watched what, and for how long. It also has a testing capability with features that can be used to survey users, as well as test for comprehension.

Books and Chapters

In Avenue parlance, each of the top-

level menus is known as a "book," while an individual training video is a "chapter." The user selects a chapter from a book; a chapter is, of course, organized as a sequence of pages. Each page consists of a video clip and optional text and graphics.

The chapter's author decides whether a given page should auto-advance to the next page upon completion of that segment or whether to give the user complete control. A standard complement of VCR-like buttons is available, including previous page, rewind, play/pause, forward, and next page. The Pages button brings up a list box of all page titles so the user can randomly go to any page. The Index button brings up an alphabetical index for the chapter to also allow random selection. The author can decide to share an index between multiple chapters; the index supports branching across chapters. The Mark button

allows the user to set bookmarks and return to them later. The More button allows the user to request more detail in the form of a nested chapter-within-a-chapter.

The course author can associate a test with a given chapter. A test is really just a specialized chapter in which each question is a page. A test page often has a full-screen bitmap that displays the question and multiple-choice answers using a mix of fonts, colors, and graphics. A test question may also be posed by a video clip associated with the page. Another video clip can be shown if the user gets an incorrect answer. The Avenue script file offers the course author a lot of flexibility in designing the test and in how to respond to user responses.

Avenue supports various ways of logging user activity. These include recording what chapters have been viewed, which pages have been seen, the time spent in each chapter or page, and the results of any tests taken. An instructor can find out not just a user's test score, but also how each question was answered, in how many attempts. There is more to Avenue than I have the space for, but the description here should give you an idea of the implementation task I faced.

Application Code

Due to space limits and proprietary considerations, I'll show only an abridged version of Avenue's source code. Hopefully, the excerpts in Listings One through Five will give you a good idea of what's involved in writing a nontrivial Liana application. There is additional code in the electronic version of this article; see "Availability," page 2.

The application consists of a main function (Listing One, page 56) and a number of classes. Similar to C, the Liana interpreter calls main after initializing the environment. Unlike C, a Liana main function initializes the application and then quickly returns. The Liana interpreter then waits until the application's windows have been closed before cleaning up and exiting. Liana automatically dispatches Windows events by calling member functions for the Liana object associated with each window.

The hierarchy of open books is stored in a Liana stack object created in the main function. The stack class (Listing Two, page 56) is derived from the Liana dynamic-array class and is a good example of how easy it is to manipulate objects in Liana. Since Liana is a typeless language, there's no need to have separate classes or templates for every type of object that you wish to store on a stack or in an array. The same class is used for the chapter stack.

The two primary classes in Avenue are bookwin (Listing Three, page 56), which manages a book window, and chapwin (Listing Four, page 56), which manages a chapter window. The touch_

menu class (available electronically) is common to both bookwin and chapwin and manages the display of a bitmap and the selection of soft buttons. A Liana displaylist is used to represent the soft buttons. Other classes in Avenue include a book_entry class, which is based on the Liana figure class, plus a user (skill) level, a path string, and a label string. There is also a figure class, which represents the rectangular area to be defined for each soft button used to bring up books. Avenue's touch_menu class is a general-purpose class that could be reused in other applications.

The chapfile class manages a single chapter file, which consists of a text file with a bunch of header lines followed by one line per page. Each page line is stored in a page object; the page objects are stored in a Liana dynamic array. The Liana range class is used to store the range of video frames for a chapter page. It is another good example of the flexibility of Liana, since the start and end of the range can be of any type: integer, real, string, or a user-defined class.

Avenue's chapter_state class is used to record the state of a chapter, including the path of the chapter file, the information about the current page of the chapter, the current video-frame number, and whether video is actually playing. The index_entry class is based on the chapter_state class because an index entry needs much of the same information, plus the text to be displayed in the index. Avenue supports index entries that branch across chapters and even to a specific frame within a page.

Finally, the Liana video class (Listing Five, page 57) encapsulates the Windows Media Control Interface (MCI) API. This class also provides useful features such as the option of displaying a bitmap over the video window. At the end of Listing Five, you can see how a DLL entry point is declared in Liana. The Avenue implementation does not use the video class directly, but instead derives class chvideo from it. This is for several reasons:

Project Assessment

Unlike many commercial projects, the implementation effort on Avenue was rather modest. As the the only developer, I started the project in late September 1992, working less than half-time. The first product shipment occurred barely four months later in early February 1993.

Avenue consists of about 3000 lines of Liana code. Based on my experience implementing Windows apps in C using the Windows SDK (Liana is one such instance), I estimate that a native C implementation of Avenue would require about 30,000 lines of code and would have taken perhaps ten times longer. Estimating a C++ implementation is a bit trickier, given the various class libraries and application frameworks, but I suspect it would require 15,000 lines of C++ and take perhaps five times longer.

Would an application generator have helped? Not really. Avenue's user interface is dynamic in nature and is not something that can be statically defined with a dialog editor, resource toolkit, or window builder.

The only real downside to using Liana is that it's a proprietary language from a one-person company (however, full source code is available). Also, some programmers prefer a more comprehensive interactive development environment (IDE). Even so, Liana's prototyping speed merits your consideration.

Figure 1: The Avenue interface.

[LISTING ONE]


// Main entry point for Avenue --- by Jack Krupansky.
// Copyright 1993 VIS Development Corporation

void main ()
{   // Access the private profile (.INI) file for AVENUE.
    env = new envfile (path (getcwd (), apname, ".ini"));

    // Determine whether status line is enabled for books.
    if ((string s = env ["enable_book_status_line"]) == "")
          enable_book_status_line = true;
    else
          enable_book_status_line = bool (int (s));

    // Create initial book window.
    window w = new bookwin (path (argv [1], apname));
    if (! w.path)
        exit ();          // Exit if we can't open the book.
    books = new stack;  // Create a stack for the book hierarchy.
    book << w;            // Put the initial book on the stack.
    w.show;               // Make the book window visible.
    // main() then returns to Liana to wait until application is
    // terminated by the user.
}


[LISTING TWO]



// Implementation of class Stack  --- by Jack Krupansky
// Copyright 1993 John W. Krupansky d/b/a Base Technology

class stack: array {
      bool isempty () {  return this.size == 0;   }
      any pop () {
            if (int i = this.size) {
              any v = this [i - 1];
              trim (1);
            }
            return v;
      }
      any push (any v) { this << v;  return v;   }
      void stack () { array (); } // constructor calls base class
      any top ()  {
          if (int i = this.size)   return this [i - 1];
          else                     return null;
      }
};


[LISTING THREE]



// Implementation of class Bookwin --- by Jack Krupansky
// Copyright 1993 VIS Development Corporation

class bookwin: touch_menu {
    publicread:
         string path;
    void bookwin (string path);
    void clicked (int i)  {
         if (i == -1)
             this.status = "Nothing selected, try again...";
         else {
              string path = this [i].path;
              if (path == "*exit")      exit_book ();
              else                      new_chapter (i);
          }
      }
      bool close ();
      void destroy ();
      void exit_book ();
      void new_chapter (int course)  {
           any c = this [course];
           if (! access (string path = c.path)) {
                 warn ("Unable to access map/chapter file: "+path);
                 this.status = "Select something...";
                 return;
            }
            this.status = "Loading "+c.text; // Let user know what we're doing
            this.cursor = "wait";            // Display the hour-glass cursor.
            window cur_map = books.top;      // Get top book from the stack.
                                             // See if we're going to another
            if (ext (path) == ".CHP") {      // book or to a chapter.
                if (chapwin       // See if we're going to the same chapter.
                    && path == chapwin.path) {

                         this.status = "Resuming chapter...";
                } else {
                          if (! chapwin)   chapwin = new chapwin;
                }
                chapwin.path = path;          // Open the new chapter.
                if (chapwin.path.size)
                    chapwin.show_maximized;  // Make the chapter visible.
      } else {
            window new_map = new bookwin (path);    // Open a new book window.
            if (new_map.path) {
                  books << new_map;      // Push the book on the stack.
                  new_map.show;          // Make the new book window visible
                  this.hide;             // Hide the current book.
             }
      }
      this.cursor = "normal";            // Restore the cursor.
    }
    void path_set (string path);
    int user_level_set (int user_level);
};

[[LISTING FOUR]


// Implementation of class Chapwin -- by Jack Krupansky.
// Copyright 1993 VIS Development Corporation

class chapwin: touch_menu {
      chapter_state incorrect_video_saved_state;
   publicread:
      chapfile chapfile;
      pushbutton play_button;
      chvideo tv;
      string path;
      int page;
      page p;
      int correct_answers;
   public:
      bool page_ended;

      void chapwin () {
           touch_menu ();           // Initialize object for parent class.
           chapters = new stack;    // Create stack for nested chapters.
                                    // Create the buttons at top of window.
           this
              << (prev_page_button = new pushbutton ("<<-", "prev_page"))
              << (play_button = new pushbutton ("Play","toggle_play_video",8))
               // ...more buttons...;
    }
    void chose (int i);
    void clear_touch_area_definitions ();
    void clicked (int i);
    void close ();
    answer_dialog get_answer_dialog (string code);
    void force_store_playing_time (bool finished);
    void go_forward ();

    void load_touch_area_definitions (string path);
    void moved (x, y);
    void next_page () {this.page++;}
    int page_finished ();
    int page_set (int page);
    void paint (x1, y1, x2, y2);
    void paint_status ();
    void paint_msg_status ();
    void paint_page_status ();
    void paint_title_status ();
    string path_set (string path);
    void pause_video ()  {
         if (tv.playing) {
             tv.pause;                          // Stop the video.
                                                // Toggle label of Play btn
             play_button.text = "Play";         // from Pause back to Play.
                                                // Display status for user.
             this.status = "Pausing video at frame "+tv.position;
             force_store_playing_time (false);  // Record user statistics.
         }
    }
    range play_range ();
    void play_video ()  {
         if (! tv.playing) toggle_play_video ();
    }
    void position (x, y);
    int position_set (int position);
    void prev_page () {this.page--;}
    void push_to_chapter ();
    void play_video ();
    void refresh ();
    void return_from_chapter ();
    void rewind_video ();
    void set_bookmark ();
    void set_button_visibility (pushbutton button, bool visible);
    void set_control_visibility (control c, bool visible);
    void show_help ();
    void show_index ();
    void show_more_detail ();
    void show_pages ();
    chapter_state state ();
    chapter_state state_set (chapter_state s);
    string status_set (string status);
    bool status_line_enabled_set (bool v) {return v;}
    void store_playing_time (bool finished);
    void toggle_play_video ()  {
         if (tv.playing)   pause_video ();
         else {
             if (page_ended)          // If user presses play at end of a page
                 this.page++;         // Then go on to the next page.
              tv.play;                // Start the video playing.
              started_play = time (); // Record the time spent playing video.
              play_button.text = "Pause"; // Change label of Play btn to Pause
              this.status = "Playing frames "      // Display status for user.
                              + tv.frames.start

                              + " to " + tv.frames.end;
      }
    }
    void video_finished ();
};


[LISTING FIVE]



// Implementation of class Video, which encapsulates the Windows
// Windows Media Control Interface (MCI)
// Copyright 1993 John W. Krupansky d/b/a Base Technology

class video: custombutton {
    static int vid_gen;
    string vid;
    timer frame_timer;
    range frames;
    bool video_visible;
 publicread:
    string path;
    bitmap bitmap;
    bitmap bitmap_set (bitmap bitmap);
    int clean_position (int pos)  {
        return max (1, min (this.size, pos));
    }
    void close ();
    void destroy ();
    void frame (int frame) {}
    range frames () {return frames.copy;}
    range frames_set (range r);
    long handle_message (
        unsigned hWnd,
        unsigned message,
        unsigned wParam,
        long lParam);
    bool mci_execute (string command);
    void mci_error (string command, string msg);
    void show_mci_error (string command, string msg);
    bool mci_execute (string command, int hWnd);
    void paint (int x1, int y1, int x2, int y2) {paint ();}

    void paint ();
    string path_set (string path);
    void pause ()  {
         if (path && ! ready () && ! stopped ()) {
                mci_execute ("pause "+vid);
               frames.start = this.position;
          }
    }
    void play () {
         pause ();
         if (path) {
             if (frames.start != null)   play (frames);
             else {
                   mci_execute ("play "+vid+" notify", handle);
                   this.video_visible = true;
                   start_timer ();
              }
         }
    }
    void play (range range) {play (range.start, range.end);}
    void play (int start, int end) {
         if (path) {
             this.frames.range (start, end);
        pause ();
        start = clean_position (start);
        end = clean_position (end);
        string cmd = "play "+vid;
        if (this.position != start)
          cmd += " from "+start;
        cmd += " to "+end+" notify";
        mci_execute (cmd, handle);
        this.video_visible = true;
        start_timer ();
      }
    }
    bool playing () {return this.status == "playing";}
    int position () {return int (status ("position"));}
    int position_set (int position)
    {
      position = clean_position (position);
      if (this.position != position) {
        if (path) {
          if (bool not_ready = ! ready ())
            pause ();
          mci_execute ("seek "+vid+" to "+position);
          frames.start = position;
          if (not_ready)
            play ();
        }
      } else
        frames.start = position;
      return position;
    }
    bool ready () {return status ("ready") == "true";}
    void rewind ();

    int size () {return int (status ("length"));}
    void start_timer ();
    string status () {return status ("mode");}
    string status (string item)
    {
      if (path) {
        string result = "";
        memory buf = malloc (256);
        string command = "status "+vid+" "+item;
                                             // call the MCI DLL entry points
        if (int rv = mciSendString (command, buf, buf.size, handle)) {
            mciGetErrorString (rv, buf, buf.size);  // another DLL entry point
            warn ("MCI Error", command+"\n\n"+buf);
        } else {
             result = ""+buf;
        }
        free (buf);
        return result;
      }
    }
    void step ();
    void stop ();
    bool stopped () {return status ("mode") == "stopped";}
    void succeeded ();
    void video ();
    void video (string path);
    bool video_visible () {return status ("video") == "on";}
    bool video_visible_set (bool video_visible);
    };
unsigned long mciSendString (
    char *lpstrCommand,
    char *lpstrReturnString,
    unsigned wReturnLength,
    unsigned hCallback) "mmsystem.dll:mciSendString"
bool mciGetErrorString (
    unsigned long wError,
    char *lpstrBuffer,
    unsigned uLength) "mmsystem.dll:mciGetErrorString"

End Listings



Copyright © 1994, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.