Difference between revisions of "User:Becky Pippen/Shared Media LSL Recipes"
Jump to navigation
Jump to search
Example #1 — using
Example #2 — using
m (→Display an image — works with animated GIFs too: Whoops... fixed extraneous space between 'data:' and 'text/html') |
m (→Make TinyURLs by script: Added <code> to the URLs and tried to get better indentation) |
||
(15 intermediate revisions by the same user not shown) | |||
Line 127: | Line 127: | ||
'''Comments:''' | '''Comments:''' | ||
* If attempting to display YouTube video full-frame, see [https://blogs.secondlife.com/community/community/tnt/blog/2010/03/16/viewer-2-tip-shared-media-show-youtube-on-a-full-prims-face this discussion]{{dead link|2024-04-29 Searched as deep as I could but didn't find it — Gwyn}}. | * If attempting to display YouTube video full-frame, see [https://blogs.secondlife.com/community/community/tnt/blog/2010/03/16/viewer-2-tip-shared-media-show-youtube-on-a-full-prims-face this discussion]{{dead link|2024-04-29 Searched as deep as I could but didn't find it — Gwyn}}. | ||
* For images, you can also use < | * For images, you can also use <syntaxhighlight lang="html" inline><image src= ></syntaxhighlight> like this: <syntaxhighlight lang="lsl2" inline>string dataURI="data:text/html,<img src='" + imageURL + "'>";</syntaxhighlight> | ||
* Note: The original Wikimedia Commons image has been deprecated and was replaced by the closest-looking one. — [[User:Gwyneth Llewelyn|Gwyneth Llewelyn]] ([[User talk:Gwyneth Llewelyn|talk]]) 04:51, 29 April 2024 (PDT) | * 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. — [[User:Gwyneth Llewelyn|Gwyneth Llewelyn]] ([[User talk:Gwyneth Llewelyn|talk]]) 04:51, 29 April 2024 (PDT) | ||
===Display a background image, tiled=== | ===Display a background image, tiled=== | ||
Line 184: | Line 184: | ||
}</syntaxhighlight> | }</syntaxhighlight> | ||
===Text colors — using | ===Text colors — using <font> element=== | ||
[[Image:MoaP-example-font-color.png|thumb|Font color]] | [[Image:MoaP-example-font-color.png|thumb|Font color]] | ||
< | <syntaxhighlight lang="lsl2" line highlight="6,8"> | ||
default | |||
{ | |||
state_entry() | |||
{ | |||
integer face = 4; | |||
string message = "<font color='Red'>Hello World</font>"; | |||
llSetPrimMediaParams(face, | |||
[PRIM_MEDIA_CURRENT_URL, "data:text/html," + message]); | |||
} | |||
}</syntaxhighlight> | |||
Comments: | '''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: | * 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: | ||
:<syntaxhighlight lang="lsl2" inline>string message = "<h2 style='color:#FF0000'>Hello World</h2>";</syntaxhighlight> | |||
===Link to external CSS file (forms example)=== | ===Link to external CSS file (forms example)=== | ||
[[Image:MoaP-example-externalCSS.png|thumb|External CSS file]] | [[Image:MoaP-example-externalCSS.png|thumb|External CSS file]] | ||
< | <syntaxhighlight lang="lsl2" line highlight="6-7,10> | ||
default | |||
{ | |||
state_entry() | |||
{ | |||
integer face = 4; | |||
string externalCSS = | |||
"https://www.google.com/css/modules/buttons/g-button-chocobo.css"; | |||
string dataURI = "data:text/html,<head><link href='</span>" + | |||
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]); | |||
} | |||
}</syntaxhighlight> | |||
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 tag < | * The tag <syntaxhighlight lang="html" inline><head></syntaxhighlight> and the attribute <syntaxhighlight lang="html" inline>type='text/css'</syntaxhighlight> may be omitted to make the data URI a few bytes shorter. | ||
===Calling external JavaScript functions (JQuery example)=== | ===Calling external JavaScript functions (JQuery example)=== | ||
[[Image:MoaP-example-JQuery.gif|thumb|Calling external JQuery]] | [[Image:MoaP-example-JQuery.gif|thumb|Calling external JQuery]] | ||
< | <syntaxhighlight lang="lsl2" line highlight="9,12-13"> | ||
integer face = 4; | |||
string externalJavascript = | |||
"https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/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]); | |||
} | |||
}</syntaxhighlight> | |||
Comments: | '''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 [ | * 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 [https://jquery.com/ 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 <syntaxhighlight lang="html" inline><script></syntaxhighlight> element. | ||
* A data URI can be used as a springboard to generate arbitrary complex web pages by making use of these elements: | * 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 < | ** a reference to external CSS with <syntaxhighlight lang="html" inline><link href=</syntaxhighlight>, | ||
** a reference to external JavaScript with < | ** a reference to external JavaScript with <syntaxhighlight lang="html" inline><script src=</syntaxhighlight>, and | ||
** a function call with parameters to the external JavaScript using < | ** a function call with parameters to the external JavaScript using <syntaxhighlight lang="html" inline><script>someFunction(args);</script></syntaxhighlight>. (In the JQuery example above, the name of the external function is <code>"$"</code>.) | ||
* See the next two examples for variations of this same technique. | * See the next two examples for variations of this same technique. | ||
===Self-served HTML and JavaScript=== | ===Self-served HTML and JavaScript=== | ||
====Example #1 — using | ====Example #1 — using <code>script src=</code> and <code>.innerHTML=</code> ==== | ||
[[Image:MoaP-example-refresh-clock.gif|thumb|Self-served auto-refreshed pages]] | [[Image:MoaP-example-refresh-clock.gif|thumb|Self-served auto-refreshed pages]] | ||
< | <syntaxhighlight lang="lsl2" line highlight="5,7,25,31-33,47,50"> | ||
integer face = 4; | |||
string myURL; | 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); | |||
} | |||
} | |||
} </syntaxhighlight> | |||
Comments: | '''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 < | * 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 <syntaxhighlight lang="HTML" inline><script src=URL></script></syntaxhighlight>. 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 < | * This works by assigning a string containing HTML to the <code>.innerHTML</code> contents of the <syntaxhighlight lang="HTML" inline><div></syntaxhighlight> element with id "clock". This achieves something similar to the Ajax-like [https://tali.appspot.com/oldhtml/scripting/scripts/bootstrapping.html 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 <syntaxhighlight lang="HTML" inline><a href=</syntaxhighlight> tag and an <syntaxhighlight lang="HTML" inline>href=</syntaxhighlight> that points to the script's HTTP-in URL. That string then replaces the <syntaxhighlight lang="HTML" inline><body></syntaxhighlight> element using: | ||
:<syntaxhighlight lang="JavaScript" inline>document.getElementsByTagName('body')[0].innerHTML = </syntaxhighlight><code><i>new-content</i></code> | |||
====Example #2 — using <body onload= (lag graph example)==== | ====Example #2 — using <code><body onload=</code> (lag graph example)==== | ||
[[Image:MoaP-example-laggraph.gif|thumb|Refreshed pages]] | [[Image:MoaP-example-laggraph.gif|thumb|Refreshed pages]] | ||
< | <syntaxhighlight lang="lsl2" line highlight="40,41"> | ||
integer face = 4; | integer face = 4; | ||
string jsURL; // where to fetch external JavaScript | string jsURL; // where to fetch external JavaScript | ||
Line 336: | Line 335: | ||
return | return | ||
"function bar(widthPct,heightPix) {" + | "function bar(widthPct,heightPix) {" + | ||
" document.writeln(\" | " document.writeln(\"<hr style='padding:0;margin:0;" + | ||
" margin-top:-1px;text-align:left;align:left;border=0;" + | " margin-top:-1px;text-align:left;align:left;border=0;" + | ||
" width:\"+widthPct+\"%;height:\"+heightPix+\"px;" + | " width:\"+widthPct+\"%;height:\"+heightPix+\"px;" + | ||
" background-color:#c22;color:#c22;'>\");}" + | " background-color:#c22;color:#c22;'>\");}" + | ||
" function graphBars(arr){for(var i=0;i | " function graphBars(arr){for(var i=0;i<arr.length;++i)" + | ||
" {bar(arr[i],18);}}"; | " {bar(arr[i],18);}}"; | ||
} | } | ||
Line 362: | Line 361: | ||
// The dataURI loads external JavaScript functions and calls one with parameters: | // The dataURI loads external JavaScript functions and calls one with parameters: | ||
string dataURI = "data:text/html," + | string dataURI = "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]); | llSetPrimMediaParams(face, [PRIM_MEDIA_CURRENT_URL, dataURI, PRIM_MEDIA_AUTO_PLAY, TRUE]); | ||
Line 378: | Line 377: | ||
} | } | ||
} | } | ||
} </ | } </syntaxhighlight> | ||
Comments: | '''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 < | * 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 <syntaxhighlight lang="html" inline><script src=...></syntaxhighlight> tag to read a file of external JavaScript functions in the <syntaxhighlight lang="html" inline><head></syntaxhighlight> element, and then the <syntaxhighlight lang="html" inline>onload=</syntaxhighlight> attribute in <syntaxhighlight lang="html" inline><body></syntaxhighlight> triggers a call to one of the functions which executes a loop creating a bunch of <syntaxhighlight lang="html" inline><hr></syntaxhighlight> 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, < | * In this example, <code>jsURL</code> is set to the script's own HTTP-in URL so that the example can be self-contained, but you can point <code>jsURL</code> to an external URL as well. If served by an external server, the JavaScript read in <syntaxhighlight lang="html" inline><head></syntaxhighlight> can be as large as the browser permits. | ||
* < | * <syntaxhighlight lang="html" inline><head></head></syntaxhighlight> can be omitted in the data URI surrounding the <syntaxhighlight lang="html" inline><script src=</syntaxhighlight> tag. | ||
===Reverse Ajax - Long-polling the HTTP-in server, chat logger example=== | ===Reverse Ajax - Long-polling the HTTP-in server, chat logger example=== | ||
Line 623: | Line 622: | ||
</tt> | </tt> | ||
Comments: | '''Comments:''' | ||
<ol start="1"> | <ol start="1"> | ||
<li>This " | <li>This "{{Wikipedia|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: | ||
: < | : <syntaxhighlight lang="lsl2" inline>sendMessage( "alert()" );</syntaxhighlight></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>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.<ref>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 <code><syntaxhighlight></code>, 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 {{mention|Becky Pippen|p=}} — [[User:Gwyneth Llewelyn|Gwyneth Llewelyn]]</ref></li> | ||
<li>To avoid some subtle WebKit problems when dynamically appending or replacing the first-level child nodes in | <li>To avoid some subtle WebKit<ref>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. — [[User:Gwyneth Llewelyn|Gwyneth Llewelyn]]</ref> problems when dynamically appending or replacing the first-level child nodes in <syntaxhighlight lang="html" inline><head></syntaxhighlight> or <syntaxhighlight lang="html" inline><body></syntaxhighlight>, we've made two special <syntaxhighlight lang="html" inline><div></syntaxhighlight> elements on the bootstrap HTML page. The first surrounds the <code>script#sc</code> element that we replace for each GET. The second is at the end of <syntaxhighlight lang="html" inline><body></syntaxhighlight> as a convenient place for the application to insert new content that would otherwise go in <syntaxhighlight lang="html" inline><body></syntaxhighlight>.</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>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 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> | <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 <syntaxhighlight lang="lsl2" inline>sendMessage()</syntaxhighlight> 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> | ||
<li>On the Javascript side, the GET is not triggered until the < | <li>On the Javascript side, the GET is not triggered until the <syntaxhighlight lang="html" inline><script></syntaxhighlight> element is attached to its parent node with <code>.appendChild()</code> or <code>.replaceChild()</code>.</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> | <li>The bootstrap Javascript above in <syntaxhighlight lang="lsl2" inline>setDataURI()</syntaxhighlight> 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> | ||
< | :<syntaxhighlight lang="html"> | ||
<!DOCTYPE HTML> | |||
<html> | |||
<body> | |||
<div> | |||
<script id='script'> </script> | |||
</div> | |||
<script> | |||
var poll=function(){ | var poll=function(){ | ||
var script=document.getElementById('script'), | var script=document.getElementById('script'), | ||
Line 659: | Line 658: | ||
script=newScript;}, | script=newScript;}, | ||
end:function() { | end: function() { | ||
clearTimeout(timeoutId); | clearTimeout(timeoutId); | ||
timeoutId=null; | timeoutId=null; | ||
Line 667: | Line 666: | ||
}; | }; | ||
}(); | }(); | ||
</script> | |||
<button id='btn' onclick=poll.beg()>Start<button> | |||
<div id='dv'> </div> | |||
</body> | |||
</html> | |||
</ | </syntaxhighlight> | ||
</ol> | </ol> | ||
===Make TinyURLs by script=== | ===Make TinyURLs by script=== | ||
< | <syntaxhighlight lang="lsl2" line highlight="15,23"> | ||
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("https://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!"); | |||
} | |||
}</syntaxhighlight> | |||
Comments: | '''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: | * 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: | ||
:<code><nowiki>http://sim4605.agni.lindenlab.com:12046/cap/2b9f06f7-431e-5b0f-9271-2d03bd15370b</nowiki></code> | |||
into a TinyURL this size: | into a TinyURL this size: | ||
:<code><nowiki>https://tinyurl.com/26dsnl2b</nowiki></code> | |||
===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. If you can't run Safari or Chrome, try the open source [ | # 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 [https://github.com/Arora/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.<ref>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 [https://github.com/chromiumembedded/cef Chromium Embedded Framework] instead. — [[User:Gwyneth Llewelyn|Gwyneth Llewelyn]]</ref> | ||
# If you don't have direct access to a web server while developing this stuff, then consider installing a local copy of [ | # If you don't have direct access to a web server while developing this stuff, then consider installing a local copy of [https://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 <code><nowiki>http://localhost/</nowiki>...</code>, and PHP scripts can simulate what the HTTP-in server would serve. That lets you, for example, edit <code>myfile.js</code> on your own computer while working on a data URI that contains <syntaxhighlight lang="HTML" inline><script src="http://localhost/myfile.js"></syntaxhighlight>. Then when <code>myfile.js</code> 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=== | ||
* [[User:Kelly_Linden/lsl_hacks|Kelly Linden's hacks]] | * [[User:Kelly_Linden/lsl_hacks|Kelly Linden's hacks]] | ||
* [ | * [https://tali.appspot.com/oldhtml/scripting/scripts/bootstrapping.html Tali Rosca's techniques] | ||
* [https:// | * [https://tali.appspot.com/oldhtml/scripting/scripts/bootstrapping.html Notecard Text on a Prim - Demo]<ref>Replaced with a link to an equivalent text.</ref> | ||
* [[llSetPrimMediaParams]]() | * [[llSetPrimMediaParams]]() | ||
* {{JIRA|SVC-3427}} | * {{JIRA|SVC-3427}}<ref>Internal issue, not accessible to most SL residents.</ref> | ||
* [https://blogs.secondlife.com/message/124144 Ajax in Shared Media #1] | * [https://blogs.secondlife.com/message/124144 Ajax in Shared Media #1]{{dead link|2024-05-01}} | ||
* [https://blogs.secondlife.com/thread/11698 Ajax in Shared Media #2] | * [https://blogs.secondlife.com/thread/11698 Ajax in Shared Media #2]{{dead link|2024-05-01}} | ||
* [https://blogs.secondlife.com/thread/12727 Ajax in Shared Media #3] | * [https://blogs.secondlife.com/thread/12727 Ajax in Shared Media #3]{{dead link|2024-05-01}} | ||
* [https://blogs.secondlife.com/thread/13455 SVG (Scalable Vector Graphics) in Shared Media] | * [https://blogs.secondlife.com/thread/13455 SVG (Scalable Vector Graphics) in Shared Media]{{dead link|2024-05-01}} | ||
* [[ | * [[Shared Media and data URI]] | ||
* [https://community.secondlife.com/t5/TNT-Second-Life-Tips-Tricks/VIEWER-2-TIP-Shared-Media-Add-custom-content-with-data-URI-no/ba-p/656903 Shared Media: Add custom content with data: URI] | * [https://community.secondlife.com/t5/TNT-Second-Life-Tips-Tricks/VIEWER-2-TIP-Shared-Media-Add-custom-content-with-data-URI-no/ba-p/656903 Shared Media: Add custom content with data: URI]{{dead link|2024-05-01}} | ||
* [https://community.secondlife.com/t5/Viewer-2-0-General/quot-Shared-Media-quot-A-few-facts/m-p/118388#M5185 "Shared Media" - A few facts] | * [https://community.secondlife.com/t5/Viewer-2-0-General/quot-Shared-Media-quot-A-few-facts/m-p/118388#M5185 "Shared Media" - A few facts]{{dead link|2024-05-01}} | ||
===Notes=== | |||
{{References}} | |||
[[Category:Shared Media]] | [[Category:Shared Media]] | ||
[[Category:LSL Examples]] | [[Category:LSL Examples]] |
Latest revision as of 03:50, 1 May 2024
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
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
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
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
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
default
{
state_entry()
{
integer face = 4;
string imageURL =
"https://commons.wikimedia.org/wiki/File:Rotating_earth_(large)_transparent.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[dead link].
- For images, you can also use
<image src= >
like this:string dataURI="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>
default
{
state_entry()
{
integer face = 4;
string imageURL =
"https://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 = "https://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 = "https://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 element
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)
default
{
state_entry()
{
integer face = 4;
string externalCSS =
"https://www.google.com/css/modules/buttons/g-button-chocobo.css";
string dataURI = "data:text/html,<head><link href='</span>" +
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 attributetype='text/css'
may be omitted to make the data URI a few bytes shorter.
Calling external JavaScript functions (JQuery example)
integer face = 4;
string externalJavascript =
"https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/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"$"
.)
- a reference to external CSS with
- See the next two examples for variations of this same technique.
Self-served HTML and JavaScript
Example #1 — using script src=
and .innerHTML=
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 anhref=
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)
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></span>" +
"<body onload=\"graphBars([" + llList2CSV(numbers) + "]);\"></body></span>";
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 theonload=
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 pointjsURL
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
// 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:
- 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 thescript#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> <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("https://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:
https://tinyurl.com/26dsnl2b
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 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, editmyfile.js
on your own computer while working on a data URI that contains<script src="http://localhost/myfile.js">
. Then whenmyfile.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
- Kelly Linden's hacks
- Tali Rosca's techniques
- Notecard Text on a Prim - Demo[4]
- llSetPrimMediaParams()
- SVC-3427[5]
- Ajax in Shared Media #1[dead link]
- Ajax in Shared Media #2[dead link]
- Ajax in Shared Media #3[dead link]
- SVG (Scalable Vector Graphics) in Shared Media[dead link]
- Shared Media and data URI
- Shared Media: Add custom content with data: URI[dead link]
- "Shared Media" - A few facts[dead link]
Notes
- ↑ 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
- ↑ Replaced with a link to an equivalent text.
- ↑ Internal issue, not accessible to most SL residents.