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.
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.
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
integerface=4;stringmyURL;integerseq=0;// sequence number for unique URLsdefault{state_entry(){llRequestURL();}http_request(keyid,stringmethod,stringbody){if(method==URL_REQUEST_GRANTED){myURL=body;llSetPrimMediaParams(face,[PRIM_MEDIA_AUTO_PLAY,TRUE,PRIM_MEDIA_CURRENT_URL,myURL]);llSetTimerEvent(5.0);}elseif(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.
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.
For images, you can also use <imagesrc=> like this: stringdataURI="data:text/html,<img src='"+imageURL+"'>";
Note: The original Wikimedia Commons image given in the code as an example has been deprecated and was replaced by the closest-looking one; if you try this code in-world, it will show up differently than the animated image to the right. — Gwyneth Llewelyn (talk) 04:51, 29 April 2024 (PDT)
Display a background image, tiled
Method #1 — using CSS in a style element in <head>
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:
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.
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 <linkhref=,
a reference to external JavaScript with <scriptsrc=, 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
integerface=4;stringmyURL;// This can be up to 2KBytes after %-hex-escaping:stringservedPage="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(){stringdataURI="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(keyid,stringmethod,stringbody){if(method==URL_REQUEST_GRANTED){myURL=body;displayPage();}elseif(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 <scriptsrc=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 <ahref= tag and an href= that points to the script's HTTP-in URL. That string then replaces the <body> element using:
Example #2 — using <body onload= (lag graph example)
Refreshed pages
integerface=4;stringjsURL;// where to fetch external JavaScriptlistnumbers;integernumSamples=50;// This is self-served in this example, but can be// moved to an external server://stringexternalJavascript(){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();integeri=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:stringdataURI="data: text/html,"+"<head><script src='"+jsURL+"'></script></head></span>"+"<body onload=\"graphBars(["+llList2CSV(numbers)+"]);\"></body></span>";llSetPrimMediaParams(face,[PRIM_MEDIA_CURRENT_URL,dataURI,PRIM_MEDIA_AUTO_PLAY,TRUE]);}http_request(keyid,stringmethod,stringbody){if(method==URL_REQUEST_GRANTED){jsURL=body;// self-serve the JavaScriptllSetTimerEvent(1.0);}elseif(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 <scriptsrc=...> 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 <scriptsrc= 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 URLkey inId = NULL_KEY; // GET request idlist 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:
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()");
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.[1]
To avoid some subtle WebKit[2] 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>.
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.
There are two timeouts hard-coded in the bootstrap data URI:
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.
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.
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.
On the Javascript side, the GET is not triggered until the <script> element is attached to its parent node with .appendChild() or .replaceChild().
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:
<!DOCTYPE HTML><html><body><div><scriptid='script'></script></div><script>varpoll=function(){varscript=document.getElementById('script'),timeoutId,seq=0,newScript;return{beg:function(){// Initiate a long-poll GETnewScript=document.createElement('script');// The response will go herenewScript.onload=poll.end;// Call poll.end() when we get a responsetimeoutId=setTimeout('poll.end()',20000);// ... or if we time outnewScript.src=' HTTP-in URL goes here /?r='+(seq++);script.parentNode.replaceChild(newScript,script);// this triggers the GETscript=newScript;},end:function(){clearTimeout(timeoutId);timeoutId=null;script.onload=null;setTimeout('poll.beg()',500);// Wait a bit before re-polling},};}();</script><buttonid='btn'onclick=poll.beg()>Start<button><divid='dv'></div></body></html>
Make TinyURLs by script
stringmyTinyURL;default{state_entry(){llRequestURL();}http_request(keyid,stringmethod,stringbody){if(method==URL_REQUEST_GRANTED){// Send our full URL to tinyurl.com for conversion// The answer will come back in http_response()llHTTPRequest("https://tinyurl.com/api-create.php?url="+body,[],"");}elseif(method=="GET"){llHTTPResponse(id,200,"Hello Real World from the Virtual World");}}http_response(keyreq,integerstat,listmet,stringbody){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:
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.[3]
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 <scriptsrc="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.
↑Unfortunately, at the time of editing/cleaning up the WikiText on this fantastic tutorial, I couldn't figure out a neat way of having three separate colours as highlights on the code. I've placed a request on the MediaWiki talk page for <syntaxhighlight>, but I'm unsure if whatever tricks they suggest will work on the SL Wiki. So, for the time being, this example will remain untouched, as originally written by @Becky Pippen — Gwyneth Llewelyn
↑It might be the case that the SL Viewer is now based on the Chromium Embedded Framework instead, so Becky's references might not apply any longer. — Gwyneth Llewelyn
↑Note: Arora seems to be a dead project these days, and I'm personally unsure if LL is still using WebKit or moved to the Chromium Embedded Framework instead. — Gwyneth Llewelyn