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

From Second Life Wiki
Jump to navigation Jump to search
(Shared Media LSL Recipes)
 
(Added recipe for Reverse Ajax: Long-polling HTTP-in (chat logger example))
Line 10: Line 10:
-->
-->


The examples here assume you're comfortable with LSL, and you just need a quick reference for the syntax of handling 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.
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===
===Display plain text — XyText replacement===
Line 212: Line 212:
             <span style="background-color:#F8F8C8">"http://www.google.com/css/modules/buttons/g-button-chocobo.css";</span>
             <span style="background-color:#F8F8C8">"http://www.google.com/css/modules/buttons/g-button-chocobo.css";</span>
   
   
         string dataURI = "data:text/html,<head><span style="background-color:#F8F8C8"><link href='</span>" +
         string dataURI = "data:text/html,&lt;head><span style="background-color:#F8F8C8">&lt;link href='</span>" +
                 <span style="background-color:#F8F8C8">externalCSS + "' rel='stylesheet' type='text/css' /></span>&lt;/head>" +
                 <span style="background-color:#F8F8C8">externalCSS + "' rel='stylesheet' type='text/css' /></span>&lt;/head>" +
                 "&lt;form>&lt;input type='button' value='Click Me' class='g-button' />&lt;/form>";
                 "&lt;form>&lt;input type='button' value='Click Me' class='g-button' />&lt;/form>";
Line 223: Line 223:
Comments:
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.
* 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 element <tt><head></tt> and the attribute <tt>type='text/css'</tt> may be omitted to make the data URI a few bytes shorter.
* The tag <tt>&lt;head></tt> and the attribute <tt>type='text/css'</tt> may be omitted to make the data URI a few bytes shorter.


===Calling external JavaScript functions (JQuery example)===
===Calling external JavaScript functions (JQuery example)===
Line 253: Line 253:
** a reference to external JavaScript with <tt>&lt;script src=</tt>, and
** a reference to external JavaScript with <tt>&lt;script src=</tt>, and
** a function call with parameters to the external JavaScript using <tt>&lt;script>someFunction(args);&lt;/script></tt>. (In the JQuery example above, the name of the external function is <tt>"$"</tt>.)
** a function call with parameters to the external JavaScript using <tt>&lt;script>someFunction(args);&lt;/script></tt>. (In the JQuery example above, the name of the external function is <tt>"$"</tt>.)
* See the next example for more of this same technique.
* See the next two examples for variations of this same technique.


===Self-served JavaScript===
===Self-served HTML and JavaScript===
====Example #1 — using &lt;script src= and .innerHTML= ====
====Example #1 — using &lt;script src= and .innerHTML= ====
[[Image:MoaP-example-refresh-clock.gif‎|thumb|Self-served auto-refreshed pages]]
[[Image:MoaP-example-refresh-clock.gif‎|thumb|Self-served auto-refreshed pages]]
<tt>
<tt>
  integer face = 4;
  integer face = 4;
  string selfServedJavascriptURL;
  string myURL;
   
   
  // This can be up to 2KBytes after %-hex-escaping:
  // This can be up to 2KBytes after %-hex-escaping:
  <span style="background-color:#F8F8C8">string servedJavascript =</span> "
  <span style="background-color:#F8F8C8">string servedPage =</span> "
  function checklength(i){if (i<10) {i='0'+i;} return i;}       
  function checklength(i){if (i<10) {i='0'+i;} return i;}       
  <span style="background-color:#F8F8C8">function clock()</span>{  
  <span style="background-color:#F8F8C8">function clock()</span>{  
Line 290: Line 290:
  {
  {
     string dataURI = "data:text/html,<span style="background-color:#F8F8C8">&lt;script src='</span>" +
     string dataURI = "data:text/html,<span style="background-color:#F8F8C8">&lt;script src='</span>" +
         <span style="background-color:#F8F8C8">selfServedJavascriptURL + "'>&lt;/script></span>" +
         <span style="background-color:#F8F8C8">myURL + "'>&lt;/script></span>" +
         "<span style="background-color:#F8F8C8">&lt;div id='clock'>&lt;script>clock();&lt;/script>&lt;/div></span>";
         "<span style="background-color:#F8F8C8">&lt;div id='clock'>&lt;script>clock();&lt;/script>&lt;/div></span>";
     llSetPrimMediaParams(face, [PRIM_MEDIA_CURRENT_URL, dataURI]);
     llSetPrimMediaParams(face, [PRIM_MEDIA_CURRENT_URL, dataURI]);
Line 305: Line 305:
     {
     {
         if (method == URL_REQUEST_GRANTED) {
         if (method == URL_REQUEST_GRANTED) {
             <span style="background-color:#F8F8C8">selfServedJavascriptURL = body;</span>
             <span style="background-color:#F8F8C8">myURL = body;</span>
             displayPage();
             displayPage();
         } else if (method == "GET") {
         } else if (method == "GET") {
             <span style="background-color:#F8F8C8">llHTTPResponse(id, 200, servedJavascript);</span>
             <span style="background-color:#F8F8C8">llHTTPResponse(id, 200, servedPage);</span>
         }
         }
     }
     }
Line 316: Line 316:
* 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 <tt>&lt;script src=URL>&lt;/script></tt>. 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.
* 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 <tt>&lt;script src=URL>&lt;/script></tt>. 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.
* 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 <tt>.innerHTML</tt> contents of the <tt>&lt;div></tt> element with id "clock". This achieves something similar to the [http://talirosca.wikidot.com/bootstrapping-html-on-a-prim technique published by Tali Rosca] and mentioned [http://wiki.secondlife.com/wiki/User:Kelly_Linden/lsl_hacks#DHTML_.2F_Javascript_.28Tali_Rosca.29 here]. In those techniques, a string is constructed containing an <tt>&lt;a href=</tt> tag and an <tt>href=</tt> that points to the script's HTTP-in URL. That string then replaces the <tt>&lt;body></tt> element using:
* This works by assigning a string containing HTML to the <tt>.innerHTML</tt> contents of the <tt>&lt;div></tt> element with id "clock". This achieves something similar to the Ajax-like [http://talirosca.wikidot.com/bootstrapping-html-on-a-prim technique published by Tali Rosca] and mentioned [http://wiki.secondlife.com/wiki/User:Kelly_Linden/lsl_hacks#DHTML_.2F_Javascript_.28Tali_Rosca.29 here]. In those techniques, a string is constructed containing an <tt>&lt;a href=</tt> tag and an <tt>href=</tt> that points to the script's HTTP-in URL. That string then replaces the <tt>&lt;body></tt> element using:
  <tt>document.getElementsByTagName('body')[0].innerHTML = <i>new-content</i></tt>
  <tt>document.getElementsByTagName('body')[0].innerHTML = <i>new-content</i></tt>


Line 362: Line 362:
         string dataURI = "data:text/html," +
         string dataURI = "data:text/html," +
             "<span style="background-color:#F8F8C8">&lt;head>&lt;script src='" + jsURL + "'>&lt;/script>&lt;/head></span>" +
             "<span style="background-color:#F8F8C8">&lt;head>&lt;script src='" + jsURL + "'>&lt;/script>&lt;/head></span>" +
             "<span style="background-color:#F8F8C8"><body onload=\"graphBars([" + llList2CSV(numbers) + "]);\">&lt;/body></span>";
             "<span style="background-color:#F8F8C8">&lt;body onload=\"graphBars([" + llList2CSV(numbers) + "]);\">&lt;/body></span>";
   
   
         llSetPrimMediaParams(face, [PRIM_MEDIA_CURRENT_URL, dataURI, PRIM_MEDIA_AUTO_PLAY, TRUE]);
         llSetPrimMediaParams(face, [PRIM_MEDIA_CURRENT_URL, dataURI, PRIM_MEDIA_AUTO_PLAY, TRUE]);
Line 382: Line 382:
* In this example, <tt>jsURL</tt> is set to the script's own HTTP-in URL so that the example can be self-contained, but you can point <tt>jsURL</tt> to an external URL as well. If served by an external server, the JavaScript read in <tt>&lt;head></tt> can be as large as the browser permits.
* In this example, <tt>jsURL</tt> is set to the script's own HTTP-in URL so that the example can be self-contained, but you can point <tt>jsURL</tt> to an external URL as well. If served by an external server, the JavaScript read in <tt>&lt;head></tt> can be as large as the browser permits.
* <tt>&lt;head>&lt;/head></tt> can be omitted in the data URI surrounding the <tt>&lt;script src=</tt> tag.
* <tt>&lt;head>&lt;/head></tt> can be omitted in the data URI surrounding the <tt>&lt;script src=</tt> tag.
===Reverse Ajax — Long-polling the HTTP-in server (chat logger example)===
[[Image:MoaP-example-Long-polling-httpin.gif|thumb|300px|Long-polling HTTP-in]]
<tt>
// Reverse Ajax: Long-polling HTTP-in.
// Becky Pippen, 2010, contributed to the public domain.
// http://wiki.secondlife.com/wiki/User:Becky_Pippen/MoaP_Snippets
integer face = 4;          // Prim face for Shared Media
<span style="background-color:#F8F8C8">string  myURL;            // HTTP-in URL</span>
<span style="background-color:#F8F8C8">key inId = NULL_KEY;      // GET request id</span>
<span style="background-color:#E0FDE0">list msgQueue = [];        // strings of Javascript</span>
<span style="background-color:#F8F8C8">// url is our own HTTP-in url.
// This sets up a bootloader web page like this:
//      &lt;html>&lt;body>
//        &lt;div>&lt;script id='sc'>&lt;/script>&lt;/div>
//        &lt;script> callbacks and poll.beg() defined here &lt;/script>
//        &lt;button onclick=poll.beg()>Start&lt;/button>
//        &lt;div id='dv'>&lt;/div>
//      &lt;/body>&lt;/html>
// When the button is pressed, the JS code sets src= on script#sc
// and reattaches the script element to the parent &lt;div> element which
// initiates a GET to the prim's HTTP-in port
//
setDataURI(string url)
{
    string dataURI = "data:text/html,
&lt;!DOCTYPE HTML>&lt;html>&lt;body>&lt;div>&lt;script id='sc'>&lt;/script>&lt;/div>&lt;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);},};}();&lt;/script>
&lt;button id='btn'onclick=poll.beg()>Start&lt;/button>&lt;div id='dv'>&lt;/div>&lt;/body>&lt;/html>";
    llSetPrimMediaParams(face, [PRIM_MEDIA_CURRENT_URL, dataURI]);</span>
}
<span style="background-color:#E0FDE0">// 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;
    while (count &lt; numMessagesQueued) {
        nextMsg = llList2String(msgQueue, count);
        nextMsgSize = llStringLength(nextMsg);
        if (totalMsgSize + nextMsgSize &lt; MAX_SIZE_CHARS) {
            totalMsg += nextMsg;
            totalMsgSize += nextMsgSize;
            ++count;
        }
    }
    // Delete the messages from the queue that we're going to send:
    if (count > 0) {
        msgQueue = llDeleteSubList(msgQueue, 0, count - 1);</span>
    }
    return totalMsg;
}
<span style="background-color:#E0FDE0">// Called when there are previous messages still queued, or if there
// is no GET request currently open to respond to.</span>
//
pushMessageToSend(string msg)
{
    <span style="background-color:#E0FDE0">msgQueue = msgQueue + [msg]; // last element is the last one stacked</span>
    // See if we can send some messages now:
    <span style="background-color:#F8F8C8">if (inId != NULL_KEY) {</span>
        <span style="background-color:#F8F8C8">llHTTPResponse(inId, 200, popQueuedMessages());</span>
        <span style="background-color:#F8F8C8">inId = NULL_KEY;</span>
    } // 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, "&lt;", "&lt; "), "\"", "\\\""), "'", "\\\'");
}
// 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:
 
    <span style="background-color:#E0FDE0">if (llGetListLength(msgQueue) == 0</span> && <span style="background-color:#F8F8C8">inId != NULL_KEY) {</span>
        // Nothing in the queue and an open GET, so respond immediately:
        <span style="background-color:#F8F8C8">llHTTPResponse(inId, 200, msg);</span>
        <span style="background-color:#F8F8C8">inId = NULL_KEY;</span>
    } <span style="background-color:#E0FDE0">else {</span>
        <span style="background-color:#E0FDE0">pushMessageToSend(msg);</span>
    }
}
// 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 &lt; numrepl; ++i) {
        msg = str_replace(msg, "{@" + (string)i + "}", llList2String(replacements, i));
    }
    <span style="background-color:#F8F8C8">sendMessage(msg);</span>
}
<span style="background-color:#E8E8FF">// Chat logger demo: writes a new &lt;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 &lt;style> elements
    // to &lt;head>, so we'll append it to an existing &lt;div> tag in &lt;body> instead.
    msg = "e('dv').innerHTML += \"{@0}\";";
    m0 = "&lt;style>td:nth-child(2) { text-align:right } tr:nth-child(odd) { background-color:#f8e8f8 }&lt;/style>";
    sendMessageF(msg, [m0]);
    // Write a &lt;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 = "&lt;table>&lt;tbody id='tbd'>&lt;/tbody>&lt;/table>";
    sendMessageF(msg, [m0]);
    llListen(0, "", NULL_KEY, "");</span>
}
default
{
    state_entry()
    {
        llClearPrimMedia(face);
<span style="background-color:#F8F8C8">        llRequestURL();</span>
         
<span style="background-color:#E8E8FF">        webAppInit();</span>
    }
<span style="background-color:#F8F8C8">    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().</span>
<span style="background-color:#E0FDE0">            if (llGetListLength(msgQueue) > 0) {</span>
<span style="background-color:#F8F8C8">                llHTTPResponse(id, 200, popQueuedMessages());
                inId = NULL_KEY;
            } else {
                inId = id;
            }
        }
    }</span>
<span style="background-color:#E8E8FF">    // 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:
    //      &lt;tr style="color:hsl(200,100%,30%)">
    //        &lt;td>[01:23]&lt;/td>
    //        &lt;td>Avatar Name&lt;/td>
    //        &lt;td>the chat text&lt;/td>
    //      &lt;/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 = "&lt;tr style=\"color: {@4}\">";
        m0 +=  "&lt;td>{@1}&lt;/td>";
        m0 +=  "&lt;td>{@2}:&lt;/td>";
        m0 +=  "&lt;td>{@3}&lt;/td>";
        m0 += "&lt;/tr>";
        string t = llGetSubString(llGetTimestamp(), 11, 15);
        sendMessageF(msg, [m0, "[" + t + "]", name, addSlashes(chat), color]);
    }</span>
}
</tt>
Comments:
<ol start="1">
<li>This "[http://en.wikipedia.org/wiki/Push_technology 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:
: <code>sendMessage( "alert()" );</code></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 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>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>
<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>
<li>On the Javascript side, the GET is not triggered until the <tt>&lt;script></tt> element is attached to its parent node with .appendChild() or .replaceChild().</li>
<li>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:</li>
<tt>
&lt;!DOCTYPE HTML>
&lt;html>
  &lt;body>
    &lt;div>
      &lt;script id='script'> &lt;/script>
    &lt;/div>
    &lt;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
          },
        };
      }();
    &lt;/script>
    &lt;button id='btn'onclick=poll.beg()>Start&lt;button>
    &lt;div id='dv'> &lt;/div>
  &lt;/body>
&lt;/html>
</tt>
</ol>


===Make TinyURLs by script===
===Make TinyURLs by script===
Line 419: Line 707:


===Development hints===
===Development hints===
# 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 [[QtWebKit|WebKit]] HTML rendering engine. Personally I like the open source [http://code.google.com/p/arora/ Arora web browser] for simulating Media-on-a-Prim: it's based on WebKit and has a handy inspect feature for browsing the HTML and debugging the JavaScript that you're developing.
# 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 [[QtWebKit|WebKit]] HTML rendering engine. If you can't run Safari or Chrome, try the open source [http://code.google.com/p/arora/ 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.
# If you don't have direct access to a web server while developing this stuff, then consider installing a local copy of [http://www.apache.org/ 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 <tt><nowiki>http://localhost/</nowiki>....</tt> That lets you, for example, edit <tt>myfile.js</tt> on your own computer while working on a data URI that contains <tt>&lt;script src="<nowiki>http://localhost/myfile.js</nowiki>"></tt>. Then when <tt>myfile.js</tt> is working, you can put the debugged JavaScript into an LSL script that will serve it through its HTTP-in port. That's easier than debugging all that in-world.
# If you don't have direct access to a web server while developing this stuff, then consider installing a local copy of [http://www.apache.org/ 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 <tt><nowiki>http://localhost/</nowiki>...</tt>, and PHP scripts can simulate what the HTTP-in server would serve. That lets you, for example, edit <tt>myfile.js</tt> on your own computer while working on a data URI that contains <tt>&lt;script src="<nowiki>http://localhost/myfile.js</nowiki>"></tt>. Then when <tt>myfile.js</tt> 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===
===References and sources===
Line 435: Line 723:
* [https://blogs.secondlife.com/community/community/tnt/blog/2010/03/17/viewer-2-tip-shared-media-add-custom-content-with-data-uri-no-webpage-upload-needed Shared Media: Add custom content with data: URI]
* [https://blogs.secondlife.com/community/community/tnt/blog/2010/03/17/viewer-2-tip-shared-media-add-custom-content-with-data-uri-no-webpage-upload-needed Shared Media: Add custom content with data: URI]
* [https://blogs.secondlife.com/message/119704 "Shared Media" - A few facts]
* [https://blogs.secondlife.com/message/119704 "Shared Media" - A few facts]
[[Category:Shared Media]]

Revision as of 22:09, 22 April 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.
// http://wiki.secondlife.com/wiki/User:Becky_Pippen/MoaP_Snippets

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;
    while (count < numMessagesQueued) {
        nextMsg = llList2String(msgQueue, count);
        nextMsgSize = llStringLength(nextMsg);

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

    // 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 when the last 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 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. 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