Jump to content

When using web on a prim, how do I call LSL functions from within javascript?


Innula Zenovka
 Share

You are about to reply to a thread that has been inactive for 302 days.

Please take a moment to consider if this thread is worth bumping.

Recommended Posts

I'm making a radio using shared media, playing tracks delivered by an external server.   I can request and play individual tracks successfully but, when one finishes I'd like to request the next one automatically.

I can do this with javascript and the HTML Audio/Video DOM ended Event but I don't really understand how to integrate that with LSL. I've studied the examples in  https://wiki.secondlife.com/wiki/User:Becky_Pippen/Shared_Media_LSL_Recipes) but I still can't really get my head round it.

What do I put inside

<script>
var vid = document.getElementById("myVideo");
vid.onended = function() {
  alert("The video has ended");
};
</script>

in place of "alert("The video has ended")" to call an lsl function? 

Or am I trying to do something that can't be done?

I do have a workaround, since I know how long each track is, so I can keep track of how long it's been playing (and account for pauses) but I'd rather use the ended event if that's possible.

 

  • Thanks 1
Link to comment
Share on other sites

The way I would do it is to have the javascript AJAX back to a file "hosted" by the prim (e.g. URL+"/do_something") which when accessed, does the thing in-world and sends a 204 (no content) response code in the HTML_response.

As for how to do that AJAX, I'd have to take a look at my HTTP example scripts I played with a while back. I was planning on putting together something about methods and best practices for MOAP scripting, but I never got around to starting/finishing the writeup.

  • Thanks 2
Link to comment
Share on other sites

Here's a worked example which calls some LSL via javascript:

Quote
key ghRequestURL;
string gsURL;

string gsMain_header =
"<!DOCTYPE xhtml>
<html xmlns=\"http://www.w3.org/1999/xhtml\">
<head><style>
.grid-container {
  display: flex;
  flex-wrap: wrap;
  background-color: teal;
  width: 1000px;
  height: 1000px;
  margin: 12px auto;
}
.grid-container div {
  background-color: rgba(255,255,255, 0.8);
  border: 1px solid rgba(0,0,0, 0.6);
  width:  123px;
  height: 123px;
}
.grid-container div:hover {
  background-color: rgba(255, 128, 128, 0.4);
}
</style></head>
<body>";
string gsMain_body;
string gsMain_footer = 
"
<script type=\"text/javascript\">
//<![CDATA[

function postB(val) {
  var xhttp = new XMLHttpRequest();
  xhttp.open(\"POST\",val+\"button\");
  xhttp.send();
} 

//]]>
</script>
</body>
</html>";

generate_page(string page, key ID)
{   
    //llOwnerSay("Debug: "+":"+page+":"+llGetHTTPHeader(ID,"x-remote-ip"));
    if(llGetSubString(page,0,0)=="/")
    {   page = llDeleteSubString(page,0,0);
    }
    if(page=="home")
    {   llSetContentType(ID,CONTENT_TYPE_XHTML);
        llHTTPResponse(ID,200,
                gsMain_header+
                gsMain_body+
                gsMain_footer);
    }else if(llGetSubString(page,-6,-1)=="button")
    {   
        integer button = (integer)page;
        integer row = button/8;
        integer col = button%8;
        vector scale = llGetScale();
        float inc_X = scale.x/8;
        float inc_Y = -scale.y/8;
        float off_X = -3.5*inc_X;
        float off_Y = -3.5*inc_Y;
        vector setPos = <off_X+(col*inc_X),off_Y+(row*inc_Y),inc_X/2+0.005>;
        
        llSetLinkPrimitiveParamsFast(2,[PRIM_POS_LOCAL,setPos]);
        
        llHTTPResponse(ID,204,"No Content.");
    }
    else
    {   llSay(0,"Unknown page: "+page);
        llHTTPResponse(ID,404,"Not found.");
    }
}


default
{   state_entry()
    {   ghRequestURL = llRequestURL();
        
        gsMain_body = "<div class =\"grid-container\">";
        integer i;
        for(i=0;i<64;++i)
        {   gsMain_body+="<div onmouseover=\"postB("+(string)i+")\">"+
                /*"button_text"+*/"\</div>\n";
        }
        gsMain_body+="</div>";
        llOwnerSay((string)llGetFreeMemory());

    }
    on_rez(integer i)
    {   llReleaseURL(gsURL);
        ghRequestURL = llRequestURL();
    }
    changed(integer c)
    {   if(c&(CHANGED_REGION_START|CHANGED_REGION))
        {   llReleaseURL(gsURL);
            ghRequestURL = llRequestURL();
        }
    }
    http_request(key ID, string Method, string Body)
    {   if(ID==ghRequestURL)
        {   llOwnerSay("Got URL: "+Body); // debug
            if (Method == URL_REQUEST_DENIED)
            {   llOwnerSay(Method);
            }else //if (Method == URL_REQUEST_GRANTED)
            {   gsURL=Body;
                llSetLinkMedia(LINK_THIS,0,
                [   PRIM_MEDIA_HOME_URL, gsURL+"/home",
                    PRIM_MEDIA_CURRENT_URL, gsURL+"/home",
                    PRIM_MEDIA_AUTO_PLAY, TRUE,
                    PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE, // don't show nav-bar.
                    PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_NONE
                    // * when an in-world MoaP does not have 'interact' permissions, their
                    // browser will immediately request the 'current URL' after navigation
                    // to a ' new page' after it recieves a response, if that response
                    // does not have status 204 (no content).
                    // * if the in-world MoaP ~Does have interact perms, navigation to a 
                    // valid page (not status 204) will cause the CURRENT_URL to change
                    // to the new page and other MoaP instances in range should
                    // avigate to that URL as well.
                ]);
            }
        }else
        {   
            string path = llGetHTTPHeader(ID,"x-path-info");
            generate_page(path,ID);
        }
    }
}

 

in this example, a webpage on a prim moves a linked child prim based on which 'button' a user is hovering over while interacting with the media. If you try it yourself, set the dimensions of a cube root prim to .5 .5 .1 and all the child prim's dimensions to .0625 ; the script goes in the root prim.

The main things to notice are the postB javascript function which is what gets called to pass on a request back to LSL by asking for a page named #button,

function postB(val) {
  var xhttp = new XMLHttpRequest();
  xhttp.open(\"POST\",val+\"button\");
  xhttp.send();
} 

and the the generate_page function which eventually gets called to handle that request, which in the case of a page of the form #button, moves the child prim and returns 204.

generate_page(string page, key ID)
{   if(llGetSubString(page,0,0)=="/") page = llDeleteSubString(page,0,0);

    if(page=="home")
    {   //...
    }else if(llGetSubString(page,-6,-1)=="button")
    {   //... move the inworld prim, then:
        llHTTPResponse(ID,204,"No Content.");
    } 
 // could add more conditionals for more pages.
    else
    {   llSay(0,"Unknown page: "+page);
        llHTTPResponse(ID,404,"Not found.");
    }
}

 

 

  • Thanks 2
Link to comment
Share on other sites

@Quistess Alpha Thanks so much!  What a fun object.   

If I properly understand the logic, postB(val) gets called in the javascript onmouseover event.   That triggers the LSL http_request event, in which generate_page fires and either delivers a clean new page or moves the child prim into position.

My application will be simpler than that, I think, since all it seems I need to do is call something similar to postB() just asking for an empty page and then, in the http_request event, if (Method == POST), ping the external server to request the next track.  

That seems suspiciously simple!  Am I missing an important point somewhere?

  • Like 1
Link to comment
Share on other sites

1 hour ago, Innula Zenovka said:

Am I missing an important point somewhere?

I think you got it, if there's any "trick" it's that you ALWAYS should send a llHTTPResponse to any http_request event you receive*, even if it's a POST type. Even if it's not ~always necessary, it saves you from being really confused and annoyed in the case where it is, and the error/problem doesn't show up for ~30 seconds after something gets mad that it didn't get a response. If there's nothing sensible to send, then llHTTPResponse(ID,204,"no content"); will do.

* with the exception of receiving your URL.

Edited by Quistess Alpha
  • Thanks 1
Link to comment
Share on other sites

12 minutes ago, Quistess Alpha said:

I think you got it, if there's any "trick" it's that you ALWAYS should send a llHTTPResponse to any http_request event you receive, even if it's a POST type. Even if it's not ~always necessary, it saves you from being really confused and annoyed in the case where it is, and the error/problem doesn't show up for ~30 seconds after something gets mad that it didn't get a response. If there's nothing sensible to send, then llHTTPResponse(ID,204,"no content"); will do.

Thanks.   Unfortunately, I'm running into problems with the llSetPrimMediaParams call, which is probably because I don't really understand what I'm doing with the HTML/XHTML

I know this basic structure works

string strHTML =
"data:text/html,
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<script src='" + externalJavascript + "'></script>
<style>
body {
    background-image: url('"+strImageURL+"');
    background-repeat: no-repeat;
    background-attachment: fixed;
    background-size: cover;
}
</style>
</head>
<body>

<h2>Title: "+strTitle+"</h2>
<p>
</p>
<h2>Artist: "+strArtistNames+"</h2>

<p>

</p>

<video id=\"myVideo\" video width=\"320\" height=\"240\" autoplay>
<source src=\""+strTrackURL+"\" type=\"audio/mpeg\">
Your browser does not support the video tag.
</video>

</body>
</html>"

That plays the tracks that my external server delivers.  I have to do something with the format of the title and artist, but at least it's displaying them.

This, however, works in that the llSetPrimMediaParams call returns STATUS_OK, but nothing appears on the face of the prim and no music plays.

"data:text/html,"+
"<!DOCTYPE xhtml>
<html xmlns=\"http://www.w3.org/1999/xhtml\">
<html>
<head>
<title>Page Title</title>
<script type=\"text/javascript\">


function postB() {
  var xhttp = new XMLHttpRequest();
  xhttp.open(\"POST\",\"button\");
  xhttp.send();
} 


<style>
body {
    background-image: url('"+strImageURL+"');
    background-repeat: no-repeat;
    background-attachment: fixed;
    background-size: cover;
}
</style>
</head>
<body>

<h2>Title: "+strTitle+"</h2>
<p>
</p>
<h2>Artist: "+strArtistNames+"</h2>

<p>

</p>

<video id=\"myVideo\" video width=\"320\" height=\"240\" autoplay>
<source src=\""+strTrackURL+"\" type=\"audio/mpeg\">
Your browser does not support the video tag.
</video>
<script>


var aud = document.getElementById(\"myVideo\");
aud.onended = postB();
</script>
</body>
</html>"

Changing the first line to "data:text/xhtml," causes the prim to display the code rather than the desired page.    

What am I doing wrong here, please?

  • Thanks 1
Link to comment
Share on other sites

2 hours ago, Innula Zenovka said:

What am I doing wrong here, please?

First off, despite being called "XMLHTTPRequest" it has nothing to do do with whether your page is in html or xhtml format. if simple html is working for you, don't change it.

That said, xhtml is better in secondlife because for some reason the devs put some heavy caveats on html pages served via media on a prim that for some reason don't apply to xhtml.

Debugging tip #1:  Look at your page in a "real" web browser.

plugging your xhtml into my script and working it around some:

Quote
key ghRequestURL;
string gsURL;

string externalJavascript;
string strTitle;
string strArtistNames;
string strTrackURL;
string strImageURL;
string gsMain_body;


integer gCounter = 0;
generate_main_body()
{   gsMain_body = "<!DOCTYPE xhtml>
<html xmlns=\"http://www.w3.org/1999/xhtml\">
<html>
<head>
<title>Page Title</title>
<script type=\"text/javascript\">


function postB() {
  var xhttp = new XMLHttpRequest();
  xhttp.open(\"POST\",\"button\");
  xhttp.send();
} 


<style>
body {
    background-image: url('"+strImageURL+"');
    background-repeat: no-repeat;
    background-attachment: fixed;
    background-size: cover;
}
</style>
</head>
<body>

<h2>Title: "+strTitle+"</h2>
<p>
</p>
<h2>Artist: "+strArtistNames+"</h2>

<p>

</p>

<video id=\"myVideo\" video width=\"320\" height=\"240\" autoplay>
<source src=\""+strTrackURL+"\" type=\"audio/mpeg\">
Your browser does not support the video tag.
</video>
<script>


var aud = document.getElementById(\"myVideo\");
aud.onended = postB();
</script>
</body>
</html>";
}
generate_response(string page, key ID, string Method)
{   
    //llOwnerSay("Debug: "+":"+page+":"+llGetHTTPHeader(ID,"x-remote-ip"));
    if(llGetSubString(page,0,0)=="/")
    {   page = llDeleteSubString(page,0,0);
    }
    if(Method=="POST")
    {   ++gCounter;
        /*  do something that changes the page here.
        */
        generate_main_body();
        llSetLinkMedia(LINK_THIS,0,
        [   PRIM_MEDIA_HOME_URL, gsURL+"/home"+(string)(++gCounter&7),
            PRIM_MEDIA_CURRENT_URL, gsURL+"/home"+(string)(++gCounter&7),
            PRIM_MEDIA_AUTO_PLAY, TRUE,
            PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE, // don't show nav-bar.
            PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_NONE
        ]);
    }else if(llGetSubString(page,0,3)=="home")
    {   llSetContentType(ID,CONTENT_TYPE_XHTML);
        llHTTPResponse(ID,200,gsMain_body);
    }
    else
    {   llSay(0,"Unknown page: "+page);
        llHTTPResponse(ID,404,"Not found.");
    }
}
default
{   state_entry()
    {   generate_main_body();
        ghRequestURL = llRequestURL();
    }
    on_rez(integer i)
    {   llReleaseURL(gsURL);
        ghRequestURL = llRequestURL();
    }
    changed(integer c)
    {   if(c&(CHANGED_REGION_START|CHANGED_REGION))
        {   llReleaseURL(gsURL);
            ghRequestURL = llRequestURL();
        }
    }
    http_request(key ID, string Method, string Body)
    {   if(ID==ghRequestURL)
        {   llOwnerSay("Got URL: "+Body+"/home0"); // debug
            if (Method == URL_REQUEST_DENIED)
            {   llOwnerSay(Method);
            }else //if (Method == URL_REQUEST_GRANTED)
            {   gsURL=Body;
                llSetLinkMedia(LINK_THIS,0,
                [   PRIM_MEDIA_HOME_URL, gsURL+"/home",
                    PRIM_MEDIA_CURRENT_URL, gsURL+"/home",
                    PRIM_MEDIA_AUTO_PLAY, TRUE,
                    PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE, // don't show nav-bar.
                    PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_NONE
                ]);
            }
        }else
        {   
            string path = llGetHTTPHeader(ID,"x-path-info");
            generate_response(path,ID,Method);
        }
    }
}

 

if you then go to the URL it spits out on state entry in a normal webbrowser, it should helpfully tell you something like:

Quote

XML Parsing Error: mismatched tag. Expected: </script>.
Location: http://simhost-0f5648160801d75a9.agni.secondlife.io:12046/cap/d70ee8a2-578a-e4e5-3eba-a9f305dea899/home0
Line Number 24, Column 3:
</head>
--^

Fixing that, and adding cdata tags (a special thing you need to do for XHTML) . . .then

Quote

XML Parsing Error: not well-formed
Location: http://simhost-0f5648160801d75a9.agni.secondlife.io:12046/cap/049559a5-57bb-4f30-a472-175e1cdfff51/home0
Line Number 36, Column 27:
<video id="myVideo" video width="320" height="240" autoplay>
--------------------------^

not sure what the standalone word 'video' is for; remove that. autoplay needs a value, so set it to true?  <source> also needs a closing tag.

fixing all that you should arrive at something like

Quote
generate_main_body()
{   gsMain_body = "<!DOCTYPE xhtml>
<html xmlns=\"http://www.w3.org/1999/xhtml\">
<head>
<title>Page Title</title>
<script type=\"text/javascript\">
//<![CDATA[
function postB() {
  var xhttp = new XMLHttpRequest();
  xhttp.open(\"POST\",\"button\");
  xhttp.send();
} 
//]]>

</script>
<style>
body {
    background-image: url('"+strImageURL+"');
    background-repeat: no-repeat;
    background-attachment: fixed;
    background-size: cover;
}
</style>
</head>
<body>

<h2>Title: "+strTitle+"</h2>
<p>
</p>
<h2>Artist: "+strArtistNames+"</h2>

<p>

</p>

<video id=\"myVideo\" width=\"320\" height=\"240\" autoplay=\"true\">
<source src=\""+strTrackURL+"\" type=\"audio/mpeg\">
Your browser does not support the video tag.
</source>
</video>
<script>
//<![CDATA[
var aud = document.getElementById(\"myVideo\");
aud.onended = postB();
//]]>
</script>
</body>
</html>";

 

 

and it seems to work. I don't have actual video/audio to test with so the page just refreshes continually.

Now, The problem with this method is going to be that everyone who watches the video/audio will send a message to secondlife's servers to refresh the page when /They/ have finished the stream. so you'll probably get one refresh per person nearby. the easy fix is to add some sort of check to not refresh for a few seconds after a refresh. Since people are likely to be a little out of sync, you might additionally want to sleep a few seconds before refreshing the page.

Happy Debugging!

ETA: you also have 2 opening HTML tags; you need exactly 1, so remove the one that doesn't have the xlmns in it.

Edited by Quistess Alpha
  • Thanks 2
Link to comment
Share on other sites

Thanks, @Quistess Alpha   I'd realised I'd messed up the tags and had come here to post my fix and then found your really helpful reply!   It's too late for me do any more tonight, but I'll work with this tomorrow.       Since this is meant to be a personal radio playing on a HUD (at least at this stage) it's not a problem that people's pages will be refreshing at different times.

This is a huge help!

I'm sorry I can't post something to test with -- I'm working with a proprietary service and I'm not allowed to share urls or passwords, but I'll see if I can upload something to Dropbox and get to work for testing purposes. 

Edited by Innula Zenovka
  • Like 1
Link to comment
Share on other sites

@Innula Zenovka Thinking about this a bit more critically, you might be able to simplify the logic a bit more by just calling window.location.reload(); (I've not tried this) in the place of alert() in your original example, If you have

PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ALL

in the llSetMediaParams (otherwise LSL would probably refresh the page twice). Then just put the 'next track' logic where you load the page.

  • Thanks 1
Link to comment
Share on other sites

14 hours ago, Quistess Alpha said:

@Innula Zenovka Thinking about this a bit more critically, you might be able to simplify the logic a bit more by just calling window.location.reload(); (I've not tried this) in the place of alert() in your original example, If you have

PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ALL

in the llSetMediaParams (otherwise LSL would probably refresh the page twice). Then just put the 'next track' logic where you load the page.

Just to make sure I understand the logic here -- in my script, each time I want to play the next track, I have to send a message to the remote server using llHTTPRequest,  read the title, artist, and various in the http_response event, and then use them to construct gsMain_body.

So presumably, whether I use window.location.reload() or function postB(), I should call llHTTPRequest in the http_request event, and then call generate_main_body() in the http_response event, when I've received the urls and other data for the new track.

  • Thanks 1
Link to comment
Share on other sites

2 minutes ago, Innula Zenovka said:

So presumably, whether I use window.location.reload() or function postB(), I should call llHTTPRequest in the http_request event, and then call generate_main_body() in the http_response event, when I've received the urls and other data for the new track.

Pretty much, the 'optimization' I'm suggesting in my last post, is a bit of a change of mindset though. With PostB/XMLHTTPRequest the logic is something like (imagine this is a flowchart) [for sake of clarity, lets call this the AJAX method]

  1. The object does some initial setup, perhaps does an initial request to your server for track info and then sets the media params.
  2. seeing the MOAP causes the user's viewer to request the page.
  3. The object receives a GET http_request asking for the main page, and responds with whatever info it has on-hand.
  4. the viewer displays the page, the track eventually ends causing the javascript "postB" to be called, thereby sending a POST http request.
  5. The object receives the POST request, then sends a request to your server.
  6. (presumably) your object gets a http_response from the server, updates internal variables to reflect the state of a new page to send, then changes the media params.
  7. When the media params are changed to a "different" URL (usually just add a number at the end, something like URL+"/home"+(++counter) ) the user's viewer detects the change and sends a GET http request.
  8. loop back to #3.

However, you can simplify that a good deal if whenever you send a page to the user, you also send a request to your server for the next track, before you need it [For sake of clarity, lets call this the 'page refresh' method]:

  1. The Object does some initial setup, requests and receives initial track info from your server, then sets the media params.
  2. Seeing the MOAP causes the user's viewer to request the page.
  3. The object receives a GET http_request asking for the main page, then
    1. sends a llHTTPResponse with whatever info it has on-hand.
    2. sends a llHTTPRequest to your server for the info on the next track.
  4. The object receives a response from your server and updates its internal info to be ready to serve the next track when requested.
  5. The viewer sees the media track end, causing the javascript window.location.reload() to refresh the page, causing a GET request.
  6. loop back to #3

Notably, the page refresh method never needs to change the media params from their initial settings( which for this method needs PRIM_MEDIA_PERMS_INTERACT to be allowed for the viewer) and should have a lower latency when requesting the next track (because the lsl to server request was already made when the current track started). This method might be a bit more fiddly to adapt to more than one observer though.

 

  • Thanks 1
Link to comment
Share on other sites

@Quistess AlphaSorry for the delay in replying.   I feel ever so dense, but could you be more specific about what step 3 involves?

In my http_response event I've got as as receiving the track info for the first track to play -- song title, artist name, image uri, track uri.  

I have it chatting out gsURL, which sets the html as I would expect, and the llSetLinkMedia call  returns STATUS_OK,

What do I do next, please?      The phrase "sends a llHTTPResponse with whatever info it has on-hand" is confusing me a bit.  

ETA  Ah -- I have it sort of working but it keeps on refreshing every couple of seconds with the AJAX method.    Same with the page refresh method.    

Here's a cut down version of what I'm using.   It plays the first couple of seconds of the track and then starts over.   Presumably I'm doing something wrong in the http_request event.

key ghRequestURL;
string gsURL;


string strTitle;
string strArtistNames;
string strTrackURL;
string strImageURL;
string gsMain_body;


integer gCounter = 0;

generate_main_body(){   
    gsMain_body = "<!DOCTYPE html>
    <html xmlns=\"http://www.w3.org/1999/xhtml\">
    <head>
    <title>Page Title</title>
    <script type=\"text/javascript\">
    //<![CDATA[
    function postB() {
        window.location.reload();
    } 
    //]]>

    </script>
    <style>
    body {
        background-image: url('"+strImageURL+"');
        background-repeat: no-repeat;
        background-attachment: fixed;
        background-size: cover;
    }
    </style>
    </head>
    <body>

    <h2>Title: "+strTitle+"</h2>
    <p>
    </p>
    <h2>Artist: "+strArtistNames+"</h2>

    <p>

    </p>

    <video id=\"myVideo\" width=\"320\" height=\"240\" autoplay=\"true\">
    <source src=\""+strTrackURL+"\" type=\"audio/mpeg\">
    Your browser does not support the video tag.
    </source>
    </video>
    <script>
    //<![CDATA[
    var aud = document.getElementById(\"myVideo\");
    aud.onended = postB();
    //]]>
    </script>
    </body>
    </html>";

}

generate_response(string page, key ID, string Method)
{   
    llOwnerSay("Debug: "+":"+page+":"+llGetHTTPHeader(ID,"x-remote-ip"));
    if(llGetSubString(page,0,0)=="/")
    {   page = llDeleteSubString(page,0,0);
    }
    if(Method=="POST")
    {   ++gCounter;
        /*  do something that changes the page here.
        */
        generate_main_body();
        llSetLinkMedia(LINK_THIS,0,
        [   PRIM_MEDIA_HOME_URL, gsURL+"/home"+(string)(++gCounter&7),
            PRIM_MEDIA_CURRENT_URL, gsURL+"/home"+(string)(++gCounter&7),
            PRIM_MEDIA_AUTO_PLAY, TRUE,
            PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE, // don't show nav-bar.
            PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE
        ]);
    }else if(llGetSubString(page,0,3)=="home")
    {   llSetContentType(ID,CONTENT_TYPE_XHTML);
        llHTTPResponse(ID,200,gsMain_body);
    }
    else
    {   llSay(0,"Unknown page: "+page);
        llHTTPResponse(ID,404,"Not found.");
    }
}




integer iIndex;

integer iPlayListTrackCounter;


key kHTTPRequestNextSongByPlaylistIdAndSessionId;


list lTemp;
list lAlreadyPlayed;



string strOwner;
string strDeviceID;




default
{   
   /* state_entry()
    {   generate_main_body();
        ghRequestURL = llRequestURL();
    }*/

    state_entry() {
        llClearPrimMedia(0);
        llReleaseURL(gsURL);
        ghRequestURL = llRequestURL();
        strOwner = (string)llGetOwner();
        strDeviceID = (string)llGetKey();
        llSetPrimMediaParams(0,[
            PRIM_MEDIA_AUTO_PLAY,TRUE,
            PRIM_MEDIA_PERMS_CONTROL,PRIM_MEDIA_PERM_ANYONE
        ]);
    }


    on_rez(integer i)
    {   llReleaseURL(gsURL);
        ghRequestURL = llRequestURL();
    }
    changed(integer c)
    {   if(c&(CHANGED_REGION_START|CHANGED_REGION))
        {   llReleaseURL(gsURL);
            ghRequestURL = llRequestURL();
        }
    }



    http_request(key ID, string Method, string Body){   
        llOwnerSay("http request: method is "+Method+" and body is "+Body);
        if(ID==ghRequestURL){   
            llOwnerSay("Got URL: "+Body+"/home0"); // debug
            if (Method == URL_REQUEST_DENIED){   
                llOwnerSay(Method);
                }
            else {//if (Method == URL_REQUEST_GRANTED)
                llOwnerSay("http_request l 292");
                gsURL=Body;
                llSetLinkMedia(LINK_THIS,0,
                [   
                PRIM_MEDIA_HOME_URL, gsURL+"/home",
                PRIM_MEDIA_CURRENT_URL, gsURL+"/home",
                PRIM_MEDIA_AUTO_PLAY, TRUE,
                PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE, // don't show nav-bar.
                PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE
                ]);
            }
        }
        else{

            string path = llGetHTTPHeader(ID,"x-path-info");
            llOwnerSay("307 method is "+Method+" and body is "+Body);
            generate_response(path,ID,Method);
            }
    }

    http_response(key request_id, integer status, list metadata, string body) {


        if (kHTTPRequestNextSongByPlaylistIdAndSessionId == request_id){
            llOwnerSay("response to kHTTPRequestNextSongByPlaylistIdAndSessionId");
            if(200 == status){
                ++iPlayListTrackCounter;
                //llOwnerSay("success\nstatus is "+(string)status+"\nbody is: "+body);
                lTemp = llParseString2List(body, ["<",">"], []);
              /*  iMax = -llGetListLength(lTemp);
                llOwnerSay("got to l 325");
                do{
                    llOwnerSay("iMax: "+(string)iMax+", "+llList2String(lTemp, iMax));
                }
                while (++iMax); */
                iIndex = llListFindList(lTemp, ["trackUrl"]);
                strTrackURL = llList2String(lTemp,iIndex + 1);

                iIndex = llListFindList(lTemp,["imageUrl"]);
                strImageURL = llList2String(lTemp, iIndex + 1);

                iIndex = llListFindList(lTemp, ["title"]);
                string str = llList2String(lTemp, (iIndex + 1));


                    strTitle = llList2String(lTemp, iIndex + 1);
                    lAlreadyPlayed+=[str];
                    iIndex = llListFindList(lTemp, ["artistNames"]);
                    strArtistNames = llList2String(lTemp, iIndex +2);

                   // fPlayDuration = fInterval +(float)llLinksetDataRead(strTitle);
                    //llSetTimerEvent(fPlayDuration);
                   // llOwnerSay("set timer for "+(string)((integer)fPlayDuration)+" seconds");
                    llOwnerSay("l 466 strTrackURL is "+strTrackURL+"\nstrImageURL is "+(string)strImageURL+"\nstrTitle is "+strTitle+"\nstrArtistNames is "+strArtistNames);
                   
                    generate_main_body();
                    llOwnerSay("gsURL is "+gsURL);



                    llReleaseURL(gsURL);
                    ghRequestURL = llRequestURL();
              
            }
        }
    }
}

 

Edited by Innula Zenovka
Link to comment
Share on other sites

4 hours ago, Innula Zenovka said:

The phrase "sends a llHTTPResponse with whatever info it has on-hand" is confusing me a bit.

I haven't been getting enough sleep lately so my ability to explain things clearly is slipping :/ here's a demo of the page refresh method with just a button instead of doing the javascript when the media ends (that sounds hard to debug.)

Debugging tip #2: if you don't understand something well enough, take your project and put it to the side. then build something simpler that uses the same concept.

trivial server script:

Quote
//
// DUMMY Server Script.
// replies to all requests with "This is a response"+ a random number
//

key ghRequestURL;
string gsURL;

string gsXHTML_home = "This is a response. ";

generate_response(string page, key ID, string Method)
{   llHTTPResponse(ID,200,gsXHTML_home+(string)(llFrand(100)));
}

default
{   
    state_entry() 
    {   llReleaseURL(gsURL);
        ghRequestURL = llRequestURL();
    }
    on_rez(integer i)
    {   llReleaseURL(gsURL);
        ghRequestURL = llRequestURL();
    }
    changed(integer c)
    {   if(c&(CHANGED_REGION_START|CHANGED_REGION))
        {   llReleaseURL(gsURL);
            ghRequestURL = llRequestURL();
        }
    }
    http_request(key ID, string Method, string Body)
    {   
        llOwnerSay("http request: method is "+Method+" and body is "+Body);
        if(ID==ghRequestURL){   
            llOwnerSay("Got URL: "+Body+"/home0"); // debug
            if (Method == URL_REQUEST_DENIED){   
                llOwnerSay(Method);
                }
            else {//if (Method == URL_REQUEST_GRANTED)
                llOwnerSay(Method);
                gsURL=Body;
            }
        }
        else
        {   string path = llGetHTTPHeader(ID,"x-path-info");
            llOwnerSay("307 method is "+Method+" and body is "+Body);
            generate_response(path,ID,Method);
        }
    }
}

 

plug the URL from that into gURL_Server:

Quote
string gURL_Server = "http://simhost-0f5648160801d75a9.agni.secondlife.io:12046/cap/bbe04758-9525-ca13-9c00-325120486847/home0";
key ghRequestURL;
string gsURL;
string gsMain_body;

generate_main_body(string sData_from_server){   
    // use the data from the server to set gsMain_body to a valid XHTML webpage.
    gsMain_body = "<!DOCTYPE html>
    <html xmlns=\"http://www.w3.org/1999/xhtml\">
    <style>
        p { font-size: 100px; }
        button { font-size: 100px; }
    </style>
    <head>
    <title>Page Title</title>
    </head>
    <body>
    <p><button onclick=\"postB()\"> reload page? </button></p>
    <p>"+sData_from_server+"</p>
    <script type=\"text/javascript\">
    //<![CDATA[
    function postB() {
        window.location.reload();
    }
    //var aud = document.getElementById(\"myVideo\");
    //aud.onended = postB();
    //]]>
    </script>
    </body>
    </html>";

}

generate_response(string page, key ID, string Method)
{   // send gsMain_body as a response to a valid HTTP request.
    llOwnerSay("Debug: "+":"+page+":"+llGetHTTPHeader(ID,"x-remote-ip"));
    if(llGetSubString(page,0,0)=="/")
    {   page = llDeleteSubString(page,0,0);
    }
    if(llGetSubString(page,0,3)=="home")
    {   llSetContentType(ID,CONTENT_TYPE_XHTML);
        llHTTPResponse(ID,200,gsMain_body);
    }else
    {   llSay(0,"Unknown page: "+page);
        llHTTPResponse(ID,404,"Not found.");
    }
}

default
{   // In the initial state, request a URL, and initial data from the server. then transition to state running.
    state_entry() {
        llClearPrimMedia(0);
        llReleaseURL(gsURL);
        //ghRequestURL = llRequestURL();
        llHTTPRequest(gURL_Server,[],""); // ask our server for some stuff.
        // it would be better form to store the handle in a global variable, but since we're only making one type of request. . .
    }
    on_rez(integer i)
    {   llClearPrimMedia(0);
        llReleaseURL(gsURL);
        llHTTPRequest(gURL_Server,[],""); // ask our server for some stuff.
    }
    http_request(key ID, string Method, string Body)
    {   if(ID==ghRequestURL)
        {   
            if (Method == URL_REQUEST_DENIED)
            {   llOwnerSay(Method);
            }else //if (Method == URL_REQUEST_GRANTED)
            {
                gsURL=Body;
                state running;
            }
        }else
        {   llOwnerSay("Bad http request ID.");
        }
    }
    http_response(key request_id, integer status, list metadata, string body)
    {   generate_main_body(body);
        ghRequestURL = llRequestURL();
    }
}

state running
{
    state_entry()
    {   llOwnerSay("State running");
        llSetLinkMedia(LINK_THIS,0,
        [   PRIM_MEDIA_HOME_URL, gsURL+"/home",
            PRIM_MEDIA_CURRENT_URL, gsURL+"/home",
            PRIM_MEDIA_AUTO_PLAY, TRUE,
            PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE, // don't show nav-bar.
            PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE
        ]);
    }
    changed(integer c)
    {   if(c&(CHANGED_REGION_START|CHANGED_REGION))
        {   state default;
        }
    }
    on_rez(integer i)
    {   state default;
    }
    http_request(key ID, string Method, string Body)
    {   string path = llGetHTTPHeader(ID,"x-path-info");
        generate_response(path,ID,Method);
        llHTTPRequest(gURL_Server,[],""); // ask our server for some stuff.
    }
    http_response(key request_id, integer status, list metadata, string body)
    {   generate_main_body(body);
    }
}

 

hope that helps!

  • Thanks 2
Link to comment
Share on other sites

Thanks, @Quistess Alpha.   I've been playing with your demo and I see how it works but with my application I'm still having difficulty getting it to wait until a track has finished before it updates the page.  I need to filter what's happening in the http_request event but I'm not yet quite sure what's triggering Method-=="GET" and what's triggering Method=="POST".  

My server is acting up so, while I'm receiving image urls I'm not reliably receiving urls for the soundtracks, so I'll have to postpone debugging further until tomorrow.   

Something to look forward to!

 

 

Link to comment
Share on other sites

2 hours ago, Innula Zenovka said:

I'm not yet quite sure what's triggering Method-=="GET" and what's triggering Method=="POST".  

IIRC a "normal" request from say, a webbrowsr or the viewer seeing your MOAP will cause a GET request will a null body. window.location.reload() should cause the same.

With the "ajax" method, you could probably fit setRequestHeader(); in there to add some other characterizing information. HTTP protocol gives you a lot of different methods of differentiating between requests. in no particular order:

  • IP address of the sender
  • the path of the URL requested
  • the query string of the url requested
    • (the bold part) www..../some/path?query=string,another=value
  • the main body of the request (if there is one).
  • custom headers in the request
  • the "method" (usually "GET" or "POST")

make sure to use one or more of those to differentiate between every different type of request, and if you're still having a hard time, have every request generate some sort of random code that it sends with the request and also logs or reports.

Edited by Quistess Alpha
  • Thanks 2
Link to comment
Share on other sites

On 12/4/2022 at 10:21 PM, Quistess Alpha said:

It's a forum, not an instant-messaging app, and we all have non-LSL things to do 😉.

Can we go back to basics here?   There's something I'm not getting.

Assume I have a function deliverPage() that will return the HTML and Javascript I need.

I call llRequestURL();..    What do I do in the subsequent http_request event?    If URL_REQUEST_GRANTED == method, then the body gives me the URL.  

 

 http_request(key id, string method, string body) {
 	 if (method == URL_REQUEST_GRANTED) {

        } else  {
          
        }
 }

I'm going round in circles with this.  If method == URL_REQUEST_GRANTED, what, in very simple terms, do I need to do next?

Link to comment
Share on other sites

17 hours ago, Innula Zenovka said:

Can we go back to basics here?   There's something I'm not getting.

So, very basics, the first thing any HTTP script that receives incoming requests (I.E. 'hosts a webpage') needs to do is to ask for a URL, where incoming requests can be sent to.

gHandleRequestURL = llRequestURL();

calling that function should trigger a http_request event where the id parameter == gHandleRequestURL, and the method is either URL_REQUEST_GRANTED or URL_REQUEST_DENIED.

If the request is granted, the body parameter will hold the url that can be used to talk to your script. You should store that url unmodified to a global variable so that you can use it later for llReleaseURL (even though it isn't needed in most cases as the url is usually released cleanly by caveat). If you're using media on a prim to display the page, you'll want to set media params when you get the url.

In other words:

 http_request(key id, string method, string body) {
 	 if (method == URL_REQUEST_GRANTED) { // expect id == gHandleRequestURL;
        // We got a URL. other scripts, web-browsers, or external servers can use that url to talk to us.
        gMyURL = body; // set a global variable.
        // I usually ownersay the URL to test in an external webbrowser:
        llOwnerSay("Recieved URL: ["+gMyURL+" LINK]");
        // optionally set MOAP to direct back at us:
        llSetLinkMedia(link,face,[
          PRIM_MEDIA_CURRENT_URL, gMyURL,
          PRIM_MEDIA_HOME_URL, gMyURL, // I like to set both, although I think only CURRENT is neccessary.
          //...
          ]);
     } else if(method == URL_REQUEST_DENIED) { // expect id == gHandleRequestURL; 
          // our request for a URL was denied. This is rare, but can happen for various reasons. 
          // something probably not too helpful will be in the body parameter, but log it anyway:
       	  llOwnerSay("Error: URL_REQUEST_DENIED:: "+body);
          // set the global variable to "" just to be safe. and so we can test against an error condition if/when we would oterwise send the URL to someone.
           gMyURL = "";
          // optionally llClearLinkMedia and set an "out of order" texture on the media face.
     } else { // expect method == GET (or POST)
          // we need to respond to the request. 
          //for demonstration, collect all info that could be useful in identifying the source and intent of the request:
          
          llOwnerSay("Method: "+method);
          llOwnerSay("Body: \n"+body+"\n- - -"); // body may potentially be many lines and quite large.
          llOwnerSay("Path: "+llGetHTTPHeader("x-path-info")); // this is my preffered differentiator, all things being equal.
          llOwnerSay("Query-string: "+llGetHTTPHeader("x-query-string")); // most likely blank.
          llOwnerSay("IP: "+llGetHTTPHeader("x-remote-ip")); // this will most likely be your own IP address, or the IP of your server.
       	    // it shouldn't be an IP of a secondlife server unless something calls llHTTPRequest on this script's URL.
          llOwnerSay("user-agent: "+llGetHTTPHeader("user-agent")); // IIRC this should tell you what web-browser is being used if any.
       
          // use that info to provide a response:
          if(send_page) { // the requester wants a webpage/ data of some kind.
             llSetContentType(id,CONTENT_TYPE_XHTML); // my usual use-case.
             llHTTPResponse(id,200,"pretend this is a valid XHTML webpage");
          }else if(do_something) { // the requester wants us to do something in-world.
             //it would be idiomatic to program your requester to have sent this as a POST request.
             do_the_thing();
             llHTTPResponse(id,204,"no-content"); // sending this response prevents things from breaking if the requester expects a response.
             // I.E. if you have a regular link on your page (ab)used as a button to cause something to happen.
          }else {
              //request was malformed or unexpected
              llHTTPResponse(id,404,"Not found.");
          }
           
     }
 }

 

17 hours ago, Innula Zenovka said:

If method == URL_REQUEST_GRANTED, what, in very simple terms, do I need to do next?

TL;DR store the body parameter as a global variable representing the prim's URL, and transmit that URL to something that can make use of it, for example llOwnerSaying it, or using it as a MOAP url.

Edited by Quistess Alpha
CONTENT_TYPE_XHTML is a builtin integer constant, not a string.
  • Thanks 1
Link to comment
Share on other sites

@Quistess AlphaThanks for the explanation.  That helps a great deal.   

I'm still confused, though (and it's my fault, not yours -- I've been staring at this too long) about how to display my html on the face of the prim once I have the prim's URL.    

If (method == REQUEST_GRANTED) I store the url as gMyURL.    I generate (or have already generated) the string strMyHTML, which I want to display on the face of the prim.  How do I put gMyURL and strMyHTML  together to make that happen?   

I've got it working, based on our earlier discussion, but I can't explain to myself the logic of what I'm doing, and I can't reliably reproduce, in different contexts, what I've done to make it work.

ETA   As an example -- and maybe the error isn't what I think it is -- I'm doing this

integer face = 4;
string myURL;
integer seq = 0; // sequence number for unique URLs
string gsMainBody;
generateMainBody(){
     gsMainBody = "

<!DOCTYPE html>
<html>
<body>
<h1>HTML DOM Events</h1>
<h2>The onclick Event</h2>

<p>The onclick event triggers a function when an element is clicked on.</p>
<p>Click to trigger a function that will output \"Hello World\":</p>

<button onclick=\"myFunction()\">Click me</button>

<p id=\"demo\"></p>

<script>
function myFunction() {
  document.getElementByid(\"demo\").innerHTML = \"Hello World\";
}
</script>

</body>
</html>";
}

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") {
            generateMainBody();
            llHTTPResponse(id, 200, "data:text/html,"+gsMainBody);
        }
    }

} 

Which results in gsMainBody being displayed as literal text on the side of the prim:

045fd39d3e14874a0d9814b03c516294.png

Edited by Innula Zenovka
Link to comment
Share on other sites

Ahh, the issue there is

  1. If you want something other than the literal text to display, you need to llSetContentType (right before llHTTPResponse()) to tell the receiving web-browser what kind of content you're sending them. (IMO llSetContentType should have been implemented as an extra argument to llHTTPResponse, I admit the way it is now is confusing.
  2. It's not obvious if your thing is a HUD or a prim rezzed in-world form your photo. If it's rezzed in-world, you should probably be using XHTML instead of HTML.

so instead of:

generateMainBody();
llHTTPResponse(id, 200, "data:text/html,"+gsMainBody);
// try instead:
generateMainBody();
llSetContentType(id, CONTENT_TYPE_HTML);
llHTTPResponse(id, 200, gsMainBody);

in LSL, you're not really allowed to manually prepped the content type to the body; you have to ask the LSL server to set the content type to one of a few specific constants.

ETA: If you'll indulge me some semantics, while the obvious visual effect of this whole process is "Putting a webpage on a face of a prim", in a "real" sense that's just smoke and mirrors. You're actually kindly asking the viewer to open a web-browser tab and slap it on the face of a prim, kind of like they're wearing some kind of AR glasses. so the 'ingredients' that go into making the system work are:

  • a URL address the user can get the webpage from (gMyURL)
  • metadata on the prim suggesting the viewer open a webbrowser and navigate to your URL (llSetMediaParams)
  • the viewer opting to comply with that suggestion (the viewer's media functionality isn't broken or disabled, and autoPlayMedia is on or the user has explicitly chosen to view the media)
  • the content of the webpage to send (gsMainBody) (and llSetContentType)

If anything breaks in the first 3, the likely failure state is nothing appearing on the prim. If anything other than the prim's underlying texture appears, it's likely some error with the content.

Edited by Quistess Alpha
  • Thanks 1
Link to comment
Share on other sites

Thank you.   That's where I was going wrong -- I didn't know about llSetContentType.   Now I understand.

It's almost there, except that the button doesn't do anything when clicked.   However, I've tried it with a different example, in a HUD, and that works rather sluggishly, so I'm not yet too bothered

generateMainBody(){
     gsMainBody = "
<!DOCTYPE html>
<html>
<body>

<h2>My First JavaScript</h2>

<button type=\"button\"
onclick=\"document.getElementById('demo').innerHTML = Date()\">
Click me to display Date and Time.</button>

<p id="demo"></p>

</body>
</html> 
";
}

If I set the content type to XHTLM it complains there's no associated stylesheet.   Does that simply mean that I need to define the CSS either inline or in the header, in the <style> element?  

  • Thanks 1
Link to comment
Share on other sites

@Quistess Alpha Follow up question -- I've been told that I will need to include some basic data for the remote server's analytics with some of my requests.     Some of this I can read only with javascript, I think -- client OS and OS number, client time offset and the like.  

Is there a way to pass values I read with javascript to LSL?  I don't think there is, but it would be good to know if I can do that.

If I can't, I'm going to have to use AJAX or fetch() to send the data rather than llHTTPRequest, and I'm not completely sure how to do this.

First, if I do it that way, do I use llSetPrimMediaParams to load a page that runs the appropriate javascript?    

And second, if this is what I do, can LSL detect the response?

Previously I would have sent the remote server my request with llHTTPRequest and then, in the subsequent http_response event,  parsed the response  and loaded the music track, album picture, song title and artist, etc, with llSetPrimMedia.  Can I still do that, or do I need to do it all with AJAX?   It looks from the W3Schools tutorials on AJAX as it that should be possible, but I'm rather hoping I don't have to do it that way!

Link to comment
Share on other sites

2 hours ago, Innula Zenovka said:

If I set the content type to XHTLM it complains there's no associated stylesheet.

That error is understandably confusing, in plain English it means you need to change the html tag to

<html xmlns=\"http://www.w3.org/1999/xhtml\">

when using XHTML. AFAIK that doesn't make your browser actually fetch any data from the w3, it's just that's the text the spec says you need to use 'to make it work', and tell your browser its an XHTML page and not, I dunno a fancy markup for cookbook recipes. (interestingly I can't find any obscure thing that would use a different xmlns value)

This took me a good hour or two of headscratching, but the reason the javascript isn't working is because getElementById is miscapitalized. If you open the page in firefox (my preffered brower) press ctrl-shift-i to open the developer toolbar. from there, go to the "console" tab to read the error in the javascript.

 

Edited by Quistess Alpha
minor typos
  • Thanks 1
Link to comment
Share on other sites

27 minutes ago, Innula Zenovka said:

Is there a way to pass values I read with javascript to LSL?  I don't think there is, but it would be good to know if I can do that.

No, but "AJAX" isn't that scary:

function SendDataToLSL(data) {
  var xhttp = new XMLHttpRequest();
  xhttp.open("POST","GetSomeData");
  xhttp.send(data);
} 

in javascript should generate a http_request event with 'method == POST' and 'body == data' and 'GetSomeData' at the end of the path string.

27 minutes ago, Innula Zenovka said:

First, if I do it that way, do I use llSetPrimMediaParams to load a page that runs the appropriate javascript?    

Sure I guess you could do that, but to wrap your head around what's happening, recall that LSL runs on SL's servers, and javascript always runs on the computer of the person viewing the page. If you llSetPrimMediaParams and nobody with media enabled is around to see it, the javascript won't run. Also, semantics, but llSetPrim... doesn't load the page, it makes a suggestion that the observer of the prim should load the page.

Edited by Quistess Alpha
  • Thanks 1
Link to comment
Share on other sites

You are about to reply to a thread that has been inactive for 302 days.

Please take a moment to consider if this thread is worth bumping.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
 Share

×
×
  • Create New...