Difference between revisions of "User:Becky Pippen/Shared Media LSL Recipes"

From Second Life Wiki
Jump to navigation Jump to search
m (remove chars that made the TOC look weird)
m (tweaks)
Line 441: Line 441:
     integer numMessagesQueued = llGetListLength(msgQueue);
     integer numMessagesQueued = llGetListLength(msgQueue);
     integer count = 0;
     integer count = 0;
     while (count < numMessagesQueued) {
    integer done = FALSE;
     while (!done && count < numMessagesQueued) {
         nextMsg = llList2String(msgQueue, count);
         nextMsg = llList2String(msgQueue, count);
         nextMsgSize = llStringLength(nextMsg);
         nextMsgSize = llStringLength(nextMsg);
Line 449: Line 450:
             totalMsgSize += nextMsgSize;
             totalMsgSize += nextMsgSize;
             ++count;
             ++count;
        } else {
            done = TRUE;
         }
         }
     }
     }
Line 624: Line 627:
<li>In the LSL listing above, <span style="background-color:#F8F8C8">the yellow highlighting</span> shows the most important parts of the HTTP long-polling mechanism. The <span style="background-color:#E0FDE0">green highlighting</span> shows the message buffering on top of that. The <span style="background-color:#E8E8FF">blue highlighting</span> is the application code that uses long-polling. To use long-polling for a different application, replace the blue parts. The rest is glue.</li>
<li>In the LSL listing above, <span style="background-color:#F8F8C8">the yellow highlighting</span> shows the most important parts of the HTTP long-polling mechanism. The <span style="background-color:#E0FDE0">green highlighting</span> shows the message buffering on top of that. The <span style="background-color:#E8E8FF">blue highlighting</span> is the application code that uses long-polling. To use long-polling for a different application, replace the blue parts. The rest is glue.</li>
<li>To avoid some subtle WebKit problems when dynamically appending or replacing the first-level child nodes in &lt;head> or &lt;body>, we've made two special &lt;div> elements on the bootstrap HTML page. The first surrounds the script#sc element that we replace for each GET. The second is at the end of &lt;body> as a convenient place for the application to insert new content that would otherwise go in &lt;body>.</li>
<li>To avoid some subtle WebKit problems when dynamically appending or replacing the first-level child nodes in &lt;head> or &lt;body>, we've made two special &lt;div> elements on the bootstrap HTML page. The first surrounds the script#sc element that we replace for each GET. The second is at the end of &lt;body> as a convenient place for the application to insert new content that would otherwise go in &lt;body>.</li>
<li>To make sure that there is nearly always a valid GET request open, the Javascript side starts a new GET after receiving a response, or when the last GET times out.</li>
<li>To make sure that there is nearly always a valid GET request open, the Javascript side starts a new GET after receiving a response, or just before the last unanswered GET times out.</li>
<li>There are two timeouts hard-coded in the bootstrap data URI:
<li>There are two timeouts hard-coded in the bootstrap data URI:
<ol style="list-style-type:lower-roman"><li>The "20000" is the timeout (20 seconds) for when an open GET expires, and should be set to something around the minimum of the WebKit outgoing GET request timeout, and the SL-LSL incoming GET timeout. This ensures that GETs are nearly continuous. [[LlHTTPResponse|This page]] says the timeout on the LSL side is 25 seconds.</li>
<ol style="list-style-type:lower-roman"><li>The "20000" is the timeout (20 seconds) for when an open GET expires, and should be set to a little less than the minimum of the WebKit outgoing GET request timeout, and the SL-LSL incoming GET timeout. This ensures that GETs are nearly continuous, and the LSL side can always respond to the most recent GET request. [[LlHTTPResponse|This page]] says the timeout on the LSL side is 25 seconds.</li>
<li>The "500" is a throttle that sets an upper limit to how fast the Javascript re-polls HTTP-in to prevent hammering the simulator. It's a half-second delay after receiving a GET response before starting a new GET.</li></ol>
<li>The "500" is a throttle that sets an upper limit to how fast the Javascript re-polls HTTP-in to prevent hammering the simulator. It's a half-second delay after receiving a GET response before starting a new GET.</li></ol>
<li> If there's a little gap between two GETs, or if two overlap a little, the sendMessage() message buffering will compensate by queuing up messages to be sent when the next GET arrives. If there are multiple messages in the queue when a GET arrives, the LSL side will concatenate as many messages as possible and send them together.
<li> If there's a little gap between two GETs, or if two overlap a little, the sendMessage() message buffering will compensate by queuing up messages to be sent when the next GET arrives. If there are multiple messages in the queue when a GET arrives, the LSL side will concatenate as many messages as possible and send them together.
Line 646: Line 649:
         newScript;
         newScript;
         return {
         return {
           beg:function(){                                      // Initiate a long-poll GET
           beg:function() {                                      // Initiate a long-poll GET
             newScript=document.createElement('script');        // The response will go here
             newScript=document.createElement('script');        // The response will go here
             newScript.onload=poll.end;                        // Call poll.end() when we get a response
             newScript.onload=poll.end;                        // Call poll.end() when we get a response
Line 654: Line 657:
             script=newScript;},
             script=newScript;},
   
   
           end:function(){
           end:function() {
             clearTimeout(timeoutId);
             clearTimeout(timeoutId);
             timeoutId=null;
             timeoutId=null;

Revision as of 10:48, 3 May 2010

How-to's with Media-on-a-Prim

The examples here assume you're comfortable with LSL, and you just need a quick reference for the syntax of handling HTML and JavaScript in LSL for Shared Media scripting. All the examples on this page are ready to copy-n-paste and drop into a prim — no other setup needed. The key syntax in each example is highlighted. There's no error handling in these minimalist examples, so add plenty of your own.

Display plain text — XyText replacement

Plain text

default
{
    state_entry()
    {
        integer face = 4;
        string message = "Hello World";
        llSetPrimMediaParams(face,
            [PRIM_MEDIA_CURRENT_URL, "data:text/html," + message]);
    }
} 

Comments:

  • An avatar must press the Reload button on the browser to see this rendered in-world. A page will be auto-loaded if you include the parameter PRIM_MEDIA_AUTO_PLAY, TRUE, and if the avatar has auto-load enabled in preferences.
  • There's no need to add any %-hex-escaping or to use llEscapeURL(); llSetPrimMediaParams(face, [PRIM_MEDIA_CURRENT_URL, url]) will automatically escape the string for you.
  • This works by feeding a data URI to the on-prim browser. Think of it as serving the contents of the page in the URL itself. For more information, see here and here.
  • The in-world browser limits the data URI (or any address) to 1024 bytes after %-hex-escaping and encoded in UTF-8. See below for tricks using external or self-served JavaScript or CSS to leverage the data URI.
  • If the data URI contains just plain text, the prefix "data:text/html," can be abbreviated "data:,".

Force a page reload

Auto-refresh
integer face = 4;
string myURL;
integer seq = 0; // sequence number for unique URLs

default
{
    state_entry()
    {
        llRequestURL();
    }

    http_request(key id, string method, string body)
    {
        if (method == URL_REQUEST_GRANTED) {
            myURL = body;
            llSetPrimMediaParams(face,
                [PRIM_MEDIA_AUTO_PLAY, TRUE,
                 PRIM_MEDIA_CURRENT_URL, myURL]);
            llSetTimerEvent(5.0);
        } else if (method == "GET") {
            llHTTPResponse(id, 200, "Sim FPS: " + (string)llGetRegionFPS());
        }
    }

    timer()
    {
        llSetPrimMediaParams(face,
            [PRIM_MEDIA_CURRENT_URL, myURL + "/?r=" + (string)(++seq)]);
    }
} 

Comments:

  • The page will reload automatically if PRIM_MEDIA_AUTO_PLAY is enabled and if the new PRIM_MEDIA_CURRENT_URL is different. This technique appends a short dummy parameter (like "/?r=12") to the end of the URL to make it different each time, forcing a reload. The parameter is otherwise ignored.

Display text with HTML markup

HTML markup

default
{
    state_entry()
    {
        integer face = 4;
        string message = "<i>Hello</i><h2>World!</h2>";
        llSetPrimMediaParams(face,
            [PRIM_MEDIA_CURRENT_URL, "data:text/html," + message]);
    }
} 

Comments:

  • The text of the data URI will be %-hex-escaped automatically for you, so there's no need to use llEscapeURL(). All you need in the data URI is the prefix data:text/html, plus your HTML.

Change window resolution

Resized web window

default
{
    state_entry()
    {
        integer face = 4;
        string message = "Hello World";
        llSetPrimMediaParams(face,
                [PRIM_MEDIA_CURRENT_URL, "data:text/html," + message,
                PRIM_MEDIA_WIDTH_PIXELS, 128,
                PRIM_MEDIA_HEIGHT_PIXELS, 32]);
    }
} 

Comments:

  • The browser will add scroll bars if the rendered HTML doesn't fit within the specified PRIM_MEDIA_* size.

Display an image — works with animated GIFs too

Image on a prim

default
{
    state_entry()
    {
        integer face = 4;
        string imageURL =
          "http://upload.wikimedia.org/wikipedia/commons/7/70/Rotating_earth_(small).gif";
        string dataURI = "data:text/html,<object data='" + imageURL + "'></object>";
        llSetPrimMediaParams(face,
            [PRIM_MEDIA_CURRENT_URL, dataURI,
             PRIM_MEDIA_WIDTH_PIXELS, 256,
             PRIM_MEDIA_HEIGHT_PIXELS, 256]);
    }
} 

Comments:

  • If attempting to display YouTube video full-frame, see this discussion.
  • For images, you can also use <image src= > like this:
string dataURI="data:text/html,<img src='" + imageURL + "'>";

Display a background image, tiled

Method #1 — using CSS in a style element in <head>

Tiled background image

default
{
    state_entry()
    {
        integer face = 4;
        string imageURL =
            "http://www.google.com/intl/en_ALL/images/logo.gif";
        string dataURI = "data:text/html,<head>"
            + "<style type='text/css'>body{background-image:url(\""
            + imageURL + "\");}</style></head><body>Hello World</body>";
        llSetPrimMediaParams(face,
                [PRIM_MEDIA_CURRENT_URL, dataURI]);
    }
} 

Method #2 — using background attribute in <body>

default
{
    state_entry()
    {
        integer face = 4;
        string imageURL = "http://www.google.com/intl/en_ALL/images/logo.gif";
        string dataURI = "data:text/html,<body background='" + imageURL + "'>"
            + "Hello World"
            + "</body>";
        llSetPrimMediaParams(face,
                [PRIM_MEDIA_CURRENT_URL, dataURI]);
    }
} 

Method #3 — using style attribute in <body>

default
{
    state_entry()
    {
        integer face = 4;
        string imageURL = "http://www.google.com/intl/en_ALL/images/logo.gif";
        string dataURI = "data:text/html,<body style='background-image:url(\"" + imageURL + "\")'>"
            + "Hello World"
            + "</body>";
        llSetPrimMediaParams(face,
                [PRIM_MEDIA_CURRENT_URL, dataURI]);
    }
} 

Text colors — using <font> element

Font color

default
{
    state_entry()
    {
        integer face = 4;
        string message = "<font color='Red'>Hello World</font>";
        llSetPrimMediaParams(face,
                [PRIM_MEDIA_CURRENT_URL, "data:text/html," + message]);
    }
}  

Comments:

  • You can also specify color and other CSS rules in a style attribute in another element. For example, you can replace message in the example above with:
string message = "<h2 style='color:#FF0000'>Hello World</h2>";

Link to external CSS file (forms example)

External CSS file

default
{
    state_entry()
    {
        integer face = 4;
        string externalCSS =
            "http://www.google.com/css/modules/buttons/g-button-chocobo.css";

        string dataURI = "data:text/html,<head><link href='" +
                externalCSS + "' rel='stylesheet' type='text/css' /></head>" +
                "<form><input type='button' value='Click Me' class='g-button' /></form>";

        llSetPrimMediaParams(face,
                [PRIM_MEDIA_CURRENT_URL, dataURI]);
    }
}  

Comments:

  • This is one way to work around the 1024-byte limitation of data URIs. The external CSS can be as large as the browser permits. Also see the examples below for putting JavaScript in an external file.
  • The tag <head> and the attribute type='text/css' may be omitted to make the data URI a few bytes shorter.

Calling external JavaScript functions (JQuery example)

Calling external JQuery

integer face = 4;
string externalJavascript =
    "http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js";
default
{
    state_entry()
    {
        string dataURI = "data:text/html," +
            "<head><script src='" + externalJavascript + "'></script></head>" +
            "<button>Toggle</button>" +
            "<p>Hello<br />World</p>" +
            "<script>$('button').click(function () " +
            "{$('p').slideToggle('slow');});</script>";

        llSetPrimMediaParams(face,
            [PRIM_MEDIA_CURRENT_URL, dataURI]);
    }
} 

Comments:

  • This is a technique for working around the 1024-byte limit for data URIs. When you can't fit all the JavaScript you need into one data URI, just move some JavaScript functions into an external file and give the browser a data URI that references the external file with a URL. In this example above, the JavaScript for JQuery lives in an external file on Google's servers. You just need enough bytes left over in the data URI to put a JavaScript function call with its parameters inside a <script> element.
  • A data URI can be used as a springboard to generate arbitrary complex web pages by making use of these elements:
    • a reference to external CSS with <link href= ,
    • a reference to external JavaScript with <script src=, and
    • a function call with parameters to the external JavaScript using <script>someFunction(args);</script>. (In the JQuery example above, the name of the external function is "$".)
  • See the next two examples for variations of this same technique.

Self-served HTML and JavaScript

Example #1 — using <script src= and .innerHTML=

Self-served auto-refreshed pages

integer face = 4;
string myURL;

// This can be up to 2KBytes after %-hex-escaping:
string servedPage = "
function checklength(i){if (i<10) {i='0'+i;} return i;}      
function clock(){ 
 var now = new Date();  
 var hours = checklength(now.getHours());  
 var minutes = checklength(now.getMinutes());  
 var seconds = checklength(now.getSeconds());  
 var format = 1; //0=24 hour format, 1=12 hour format   
 var time;  
 if (format == 1) { 
  if (hours >= 12) {  
   if (hours ==12 ) { hours = 12;
   } else { hours = hours-12; } 
   time=hours+':'+minutes+':'+seconds+' PM';   
  } else if(hours < 12) { 
   if (hours ==0) {hours=12;}   
   time=hours+':'+minutes+':'+seconds+' AM';   
  }   
 }  
 if (format == 0) {time= hours+':'+minutes+':'+seconds;}   
 document.getElementById('clock').innerHTML=time;
 setTimeout('clock();', 500); 
} "; // yes, that's one long string.

displayPage()
{
    string dataURI = "data:text/html,<script src='" +
        myURL + "'></script>" +
        "<div id='clock'><script>clock();</script></div>";
    llSetPrimMediaParams(face, [PRIM_MEDIA_CURRENT_URL, dataURI]);
}

default
{
    state_entry()
    {
        llRequestURL();
    }

    http_request(key id, string method, string body)
    {
        if (method == URL_REQUEST_GRANTED) {
            myURL = body;
            displayPage();
        } else if (method == "GET") {
            llHTTPResponse(id, 200, servedPage);
        }
    }
} 

Comments:

  • This is a technique for trading the 1024-byte limit of data URIs for the 2048-byte-per-page limit that the script can serve itself. As in the previous example, when you can't fit all the JavaScript into one data URI, just move the JavaScript into an external file and reference the file with <script src=URL></script>. When the URL refers to the script's own HTTP-in URL, then the page served can contain up to 2K bytes, so you get at least that many bytes plus whatever logic you can fit into the rest of the data URI. If you move that JavaScript to an external web server, the size of the external JavaScript file can be as large as the client web browser can render.
  • Your script can serve an arbitrary number of different pages through one HTTP-in URL by appending a path or parameter to the URL.
  • This works by assigning a string containing HTML to the .innerHTML contents of the <div> element with id "clock". This achieves something similar to the Ajax-like technique published by Tali Rosca and mentioned here. In those techniques, a string is constructed containing an <a href= tag and an href= that points to the script's HTTP-in URL. That string then replaces the <body> element using:
document.getElementsByTagName('body')[0].innerHTML = new-content

Example #2 — using <body onload= (lag graph example)

Refreshed pages

integer face = 4;
string jsURL; // where to fetch external JavaScript
list numbers;
integer numSamples = 50;

// This is self-served in this example, but can be
// moved to an external server:
//
string externalJavascript()
{
    return
        "function bar(widthPct,heightPix) {" +
        " document.writeln(\"<hr style='padding:0;margin:0;" +
        "  margin-top:-1px;text-align:left;align:left;border=0;" +
        "  width:\"+widthPct+\"%;height:\"+heightPix+\"px;" +
        "  background-color:#c22;color:#c22;'>\");}" +
        " function graphBars(arr){for(var i=0;i<arr.length;++i)" +
        "  {bar(arr[i],18);}}";
}

default
{
    state_entry()
    {
        llClearPrimMedia(face);
        llRequestURL();
        integer i = numSamples;
        while (--i >= 0) {
            numbers += 0;
        }
    }

    timer()
    {
        numbers = llList2List(numbers, 1, -1) + [(integer)(6.0 * (45.0 - llGetRegionFPS()))];

        // The dataURI loads external JavaScript functions and calls one with parameters:

        string dataURI = "data:text/html," +
            "<head><script src='" + jsURL + "'></script></head>" +
            "<body onload=\"graphBars([" + llList2CSV(numbers) + "]);\"></body>";

        llSetPrimMediaParams(face, [PRIM_MEDIA_CURRENT_URL, dataURI, PRIM_MEDIA_AUTO_PLAY, TRUE]);
    }

    http_request(key id, string method, string body)
    {
        if (method == URL_REQUEST_GRANTED) {
            jsURL = body; // self-serve the JavaScript
            llSetTimerEvent(1.0);
        } else if (method == "GET") {
            llHTTPResponse(id, 200, externalJavascript());
        }
    }
} 

Comments:

  • Like the previous example, this is another variation of how to structure a data URI to serve as a springboard for arbitrarily large pages. In this example, the data URI first uses a <script src=...> tag to read a file of external JavaScript functions in the <head> element, and then the onload= attribute in <body> triggers a call to one of the functions which executes a loop creating a bunch of <hr> elements that form the bar chart. The size of HTML generated by the JavaScript can be as large as the browser permits.
  • In this example, jsURL is set to the script's own HTTP-in URL so that the example can be self-contained, but you can point jsURL to an external URL as well. If served by an external server, the JavaScript read in <head> can be as large as the browser permits.
  • <head></head> can be omitted in the data URI surrounding the <script src= tag.

Reverse Ajax - Long-polling the HTTP-in server, chat logger example

Long-polling HTTP-in

// Reverse Ajax: Long-polling HTTP-in.
// Becky Pippen, 2010, contributed to the public domain.

integer face = 4;          // Prim face for Shared Media

string  myURL;             // HTTP-in URL
key inId = NULL_KEY;       // GET request id
list msgQueue = [];        // strings of Javascript

// url is our own HTTP-in url.
// This sets up a bootloader web page like this:
//      <html><body>
//         <div><script id='sc'></script></div>
//         <script> callbacks and poll.beg() defined here </script>
//         <button onclick=poll.beg()>Start</button>
//         <div id='dv'></div>
//      </body></html>
// When the button is pressed, the JS code sets src= on script#sc
// and reattaches the script element to the parent <div> element which
// initiates a GET to the prim's HTTP-in port
//
setDataURI(string url)
{
    string dataURI = "data:text/html,
<!DOCTYPE HTML><html><body><div><script id='sc'></script></div><script>
var poll=function(){var sc=document.getElementById('sc'),t2,seq=0,s0;return{
beg:function(){s0=document.createElement('script');s0.onload=poll.end;t2=setTimeout('poll.end()',20000);
s0.src='" + url + "/?r='+(seq++);sc.parentNode.replaceChild(s0,sc);sc=s0;},
end:function(){clearTimeout(t2);t2=null;sc.onload=null;setTimeout('poll.beg()',500);},};}();</script>
<button id='btn'onclick=poll.beg()>Start</button><div id='dv'></div></body></html>";

    llSetPrimMediaParams(face, [PRIM_MEDIA_CURRENT_URL, dataURI]);
}

// Returns zero or more queued messages. Assumes no single message is
// longer than MAX_SIZE_CHARS (will hang if there is)
//
string popQueuedMessages()
{
    string  totalMsg = "";
    integer totalMsgSize = 0;
    string  nextMsg = "";
    integer nextMsgSize = 0;

    // HTTP response bodies are limited to 2048 bytes after encoding
    // in UTF-8. LSL string sizes are measured in characters, which,
    // in UTF-8, use one byte (for ASCII chars), two bytes (most Latin-1),
    // or three bytes (a few international characters). So, unless
    // you re-write this section so that it measures UTF-8 size, keep
    // MAX_SIZE_CHARS small enough so the text will fit in a response body.
    //
    integer MAX_SIZE_CHARS = 1000; // Max HTTP body size 
 
    integer numMessagesQueued = llGetListLength(msgQueue);
    integer count = 0;
    integer done = FALSE;
    while (!done && count < numMessagesQueued) {
        nextMsg = llList2String(msgQueue, count);
        nextMsgSize = llStringLength(nextMsg);

        if (totalMsgSize + nextMsgSize < MAX_SIZE_CHARS) {
            totalMsg += nextMsg;
            totalMsgSize += nextMsgSize;
            ++count;
        } else {
            done = TRUE;
        }
    }

    // Delete the messages from the queue that we're going to send:
    if (count > 0) {
        msgQueue = llDeleteSubList(msgQueue, 0, count - 1);
    }

    return totalMsg;
}

// Called when there are previous messages still queued, or if there
// is no GET request currently open to respond to.
//
pushMessageToSend(string msg)
{
    msgQueue = msgQueue + [msg]; // last element is the last one stacked

    // See if we can send some messages now:
    if (inId != NULL_KEY) {
        llHTTPResponse(inId, 200, popQueuedMessages());
        inId = NULL_KEY;
    } // else wait for the next incoming GET request
}

// Replaces all occurrences of 'from' with 'to' in 'src'
// From http://snipplr.com/view/13279/lslstrreplace/
//
string str_replace(string subject, string search, string replace)
{
    return llDumpList2String(llParseStringKeepNulls(subject, [search], []), replace);
}

// Optionally filter out characters in text that would mess up the
// web page display. This demo just escapes ' and " and adds a space after '<'.
//
string addSlashes(string s)
{
    return str_replace(str_replace(str_replace(s, "<", "< "), "\"", "\\\""), "'", "\\\'");
}

// This is the main interface for LSL to control the Shared Media web page.
// The messages we send consist of Javascript function statements that the
// browser will evaluate and execute in the context of the web page.
// See sendMessageF() for a similar function with macro replacement.
//
sendMessage(string msg)
{
    // Test for the easy case: if there are no other messages waiting
    // in the queue, and if there is an open GET connection, then just
    // respond immediately:
 
    if (llGetListLength(msgQueue) == 0 && inId != NULL_KEY) {
        // Nothing in the queue and an open GET, so respond immediately:
        llHTTPResponse(inId, 200, msg);
        inId = NULL_KEY;
    } else {
        pushMessageToSend(msg);
    }
}

// Same as sendMessage() but with macro replacements. For each nth string
// element in replacements, replace all occurrences of {@n}. For example,
//     sendMessageF( "alert('{@0} {@1}!')", ["Hello", "World"] );
// will send "alert('Hello World!')" to the web browser.
//
sendMessageF(string msg, list replacements)
{
    integer numrepl = llGetListLength(replacements);
    integer i;
    for (i = 0; i < numrepl; ++i) {
        msg = str_replace(msg, "{@" + (string)i + "}", llList2String(replacements, i));
    }

    sendMessage(msg);
}

// Chat logger demo: writes a new <tr> table row to the web page
// for every line of open chat it hears.
//
webAppInit()
{
    string msg;
    string m0;

    // First, send over a few handy function definitions:

    msg = "function $$(t) { return document.getElementsByTagName(t)[0]; };";
    msg += "function h() { return $$('head'); };";
    msg += "function b() { return $$('body'); };";
    msg += "function e(id) { return document.getElementById(id); };";
    sendMessage(msg);

    // Send some CSS. WebKit is sensitive about appending <style> elements
    // to <head>, so we'll append it to an existing <div> tag in <body> instead.

    msg = "e('dv').innerHTML += \"{@0}\";";
    m0 = "<style>td:nth-child(2) { text-align:right } tr:nth-child(odd) { background-color:#f8e8f8 }</style>";
    sendMessageF(msg, [m0]);

    // Write a <table> element into element div#dv. The lines of chat will
    // become rows in this table appended to tbody#tbd

    msg = "e('dv').innerHTML += \"{@0}\";";
    m0 = "<table><tbody id='tbd'></tbody></table>";
    sendMessageF(msg, [m0]);

    llListen(0, "", NULL_KEY, "");
}

default
{
    state_entry()
    {
        llClearPrimMedia(face);
        llRequestURL();
         
        webAppInit();
    }

    http_request(key id, string method, string body)
    {
        if (method == URL_REQUEST_GRANTED) {
            myURL = body;
            llOwnerSay("myURL=" + myURL);
            setDataURI(myURL);
        } else if (method == "GET") {
            // Either send some queued messages now with llHTTPResponse(),
            // or if there's nothing to do now, save the GET id and
            // wait for somebody to call sendMessage().
            if (llGetListLength(msgQueue) > 0) {
                llHTTPResponse(id, 200, popQueuedMessages());
                inId = NULL_KEY;
            } else {
                inId = id;
            }
        }
    }

    // When we hear chat from name, send a Javascript statement that
    // appends HTML to element #tbd. I.e., we'll make a string of HTML
    // formatted like this:
    //      <tr style="color:hsl(200,100%,30%)">
    //         <td>[01:23]</td>
    //         <td>Avatar Name</td>
    //         <td>the chat text</td>
    //      </tr>
    // and send it wrapped it in a Javascript statement like this:
    //      e('tbd').innerHTML += htmlstring;
    //
    listen(integer chan, string name, key id, string chat)
    {
        integer s = (integer)("0x" + llGetSubString((string)id, 0, 6));
        string hue = (string)(s % 360);
        string color = "hsl(" + hue + ",100%, 30%)";

        string msg = "e('tbd').innerHTML += '{@0}';";
        string m0 = "<tr style=\"color: {@4}\">";
        m0 +=   "<td>{@1}</td>";
        m0 +=   "<td>{@2}:</td>";
        m0 +=   "<td>{@3}</td>";
        m0 += "</tr>";

        string t = llGetSubString(llGetTimestamp(), 11, 15);
        sendMessageF(msg, [m0, "[" + t + "]", name, addSlashes(chat), color]);
    }
}

Comments:

  1. This "server-push" technique using long-polling gives an LSL script the ability to modify the web page displayed on a prim. The Javascript side always keeps an HTTP GET open to the prim's HTTP-in URL, and the LSL side responds whenever it wants to with a response consisting of strings of Javascript to be executed by the web browser. For example, this LSL code causes an alert box to pop up on the web page:
    sendMessage( "alert()" );
  2. In the LSL listing above, the yellow highlighting shows the most important parts of the HTTP long-polling mechanism. The green highlighting shows the message buffering on top of that. The blue highlighting is the application code that uses long-polling. To use long-polling for a different application, replace the blue parts. The rest is glue.
  3. To avoid some subtle WebKit problems when dynamically appending or replacing the first-level child nodes in <head> or <body>, we've made two special <div> elements on the bootstrap HTML page. The first surrounds the script#sc element that we replace for each GET. The second is at the end of <body> as a convenient place for the application to insert new content that would otherwise go in <body>.
  4. To make sure that there is nearly always a valid GET request open, the Javascript side starts a new GET after receiving a response, or just before the last unanswered GET times out.
  5. There are two timeouts hard-coded in the bootstrap data URI:
    1. The "20000" is the timeout (20 seconds) for when an open GET expires, and should be set to a little less than the minimum of the WebKit outgoing GET request timeout, and the SL-LSL incoming GET timeout. This ensures that GETs are nearly continuous, and the LSL side can always respond to the most recent GET request. This page says the timeout on the LSL side is 25 seconds.
    2. The "500" is a throttle that sets an upper limit to how fast the Javascript re-polls HTTP-in to prevent hammering the simulator. It's a half-second delay after receiving a GET response before starting a new GET.
  6. If there's a little gap between two GETs, or if two overlap a little, the sendMessage() message buffering will compensate by queuing up messages to be sent when the next GET arrives. If there are multiple messages in the queue when a GET arrives, the LSL side will concatenate as many messages as possible and send them together.
  7. On the Javascript side, the GET is not triggered until the <script> element is attached to its parent node with .appendChild() or .replaceChild().
  8. The bootstrap Javascript above in setDataURI() had to be compressed somewhat to fit into the 1024-byte data URI limit. Here's an expanded listing for reference with more descriptive names:
  9. <!DOCTYPE HTML> <html> <body> <div> <script id='script'> </script> </div> <script> var poll=function(){ var script=document.getElementById('script'), timeoutId, seq=0, newScript; return { beg:function() { // Initiate a long-poll GET newScript=document.createElement('script'); // The response will go here newScript.onload=poll.end; // Call poll.end() when we get a response timeoutId=setTimeout('poll.end()',20000); // ... or if we time out newScript.src=' HTTP-in URL goes here /?r='+(seq++); script.parentNode.replaceChild(newScript,script); // this triggers the GET script=newScript;}, end:function() { clearTimeout(timeoutId); timeoutId=null; script.onload=null; setTimeout('poll.beg()',500); // Wait a bit before re-polling }, }; }(); </script> <button id='btn'onclick=poll.beg()>Start<button> <div id='dv'> </div> </body> </html>

Make TinyURLs by script

string myTinyURL;

default
{
    state_entry()
    {
        llRequestURL();
    }

    http_request(key id, string method, string body)
    {
        if (method == URL_REQUEST_GRANTED) {
            // Send our full URL to tinyurl.com for conversion
            // The answer will come back in http_response()
            llHTTPRequest("http://tinyurl.com/api-create.php?url=" + body, [], "");
        } else if (method == "GET") {
            llHTTPResponse(id, 200, "Hello Real World from the Virtual World");
        }
    }

    http_response(key req, integer stat, list met, string body)
    {
        myTinyURL = body;
        llOwnerSay("My HTTP-in TinyURL is: " + myTinyURL + " , Click Me!");
    }
} 

Comments:

  • Any URL or data URI can be mapped to a TinyURL. The example above reduces an assigned HTTP-in URL, which is a long one such as:
http://sim4605.agni.lindenlab.com:12046/cap/2b9f06f7-431e-5b0f-9271-2d03bd15370b

into a TinyURL this size:

http://tinyurl.com/y9etul3

Development hints

  1. You can develop your HTML and JavaScript outside of Second Life by putting your JavaScript incantations in data URIs that you feed to any web browser. For the most compatible browsers, use Safari or Google Chrome, or any other that uses the WebKit HTML rendering engine. If you can't run Safari or Chrome, try the open source Arora web browser for simulating Media-on-a-Prim: it's based on straight-up WebKit and has the same handy inspector/debugger found in other WebKit browsers.
  2. If you don't have direct access to a web server while developing this stuff, then consider installing a local copy of Apache web server on your computer. You can restrict it to local access. Then you can refer to files on your own computer with URLs that start with http://localhost/..., and PHP scripts can simulate what the HTTP-in server would serve. That lets you, for example, edit myfile.js on your own computer while working on a data URI that contains <script src="http://localhost/myfile.js">. Then when myfile.js is working, you can put the debugged code into an LSL script that will serve it through its HTTP-in port. That's easier than debugging all that in-world.

References and sources