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 308 days.

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

Recommended Posts

@Quistess AlphaThanks so much both for spotting the capitalisation error (the example wasn't vital, obviously, but it was bugging me that it didn't work) and for explaining the misleading error message.

Thank you, too, for explaining AJAX to me.  That seems rather simpler than I feared .   

If I properly understand it, when I get to the stage where I need to send a page of data analytics to the remote server, I use llSetPrimMediaParams to ask the user's viewer to load a page that runs a script which both collects the data and then calls your function SendDataToLSL(data).   This triggers an http_request event with POST as the method,  in which I call llHttpRequest to send the body that I've just received, plus my credentials and some other stuff as headers, to the remote server.  

I'll give that a try tomorrow and see what happens.   This analytics business was sprung on me yesterday, and I'm still trying to get my head round it!

Link to comment
Share on other sites

48 minutes ago, Innula Zenovka said:

(the example wasn't vital, obviously, but it was bugging me that it didn't work)

Often the important part isn't the result, but the process. I also learned that the problem ~wasn't any of the two or three other minor differences between my 'known working example' and the page that didn't appear to do the javascript. (*Apparently CDATA tags aren't always necessary, and the script tags don't need to specify the language as javascript)

48 minutes ago, Innula Zenovka said:

If I properly understand it, when I get to the stage where I need to send a page of data analytics to the remote server, I use llSetPrimMediaParams to ask the user's viewer to load a page that runs a script which both collects the data and then calls your function SendDataToLSL(data).   This triggers an http_request event with POST as the method,  in which I call llHttpRequest to send the body that I've just received, plus my credentials and some other stuff as headers, to the remote server.  

That would work, but 2 things to consider:

  1. llSetMediaParams isn't the only way to get the user to load a page. If this were a thing in-world, you might consider just llRegionSayTo() the URL of the page that runs the script to the person with the credentials (and ask them to open it). for a non-HUD anyone in range (which is indeterminate and potentially even extends over region borders) may load the page if it's set as prim media.
  2. AJAX can also, I believe, set custom headers. You might consider sending the response directly to the server, rather than forwarding it back to SL unless the LSL ~also needs the data. (Now I think about it more, not sending your server-credentials in the page seems like a reasonable reason to ping back to LSL)
48 minutes ago, Innula Zenovka said:

This analytics business was sprung on me yesterday,

What kind of analytics are you doing? Collecting data that can't be obtained via LSL on people automatically seems morally dubious to me without more context.

Edited by Quistess Alpha
Link to comment
Share on other sites

42 minutes ago, Quistess Alpha said:

Often the important part isn't the result, but the process. I also learned that the problem ~wasn't any of the two or three other minor differences between my 'known working example' and the page that didn't appear to do the javascript. (*Apparently CDATA tags aren't always necessary, and the script tags don't need to specify the language as javascript)

That would work, but 2 things to consider:

  1. llSetMediaParams isn't the only way to get the user to load a page. If this were a thing in-world, you might consider just llRegionSayTo() the URL of the page that runs the script to the person with the credentials (and ask them to open it). for a non-HUD anyone in range (which is indeterminate and potentially even extends over region borders) may load the page if it's set as prim media.
  2. AJAX can also, I believe, set custom headers. You might consider sending the response directly to the server, rather than forwarding it back to SL unless the LSL ~also needs the data. (Now I think about it more, not sending your server-credentials in the page seems like a reasonable reason to ping back to LSL)

What kind of analytics are you doing? Collecting data that can't be obtained via LSL on people automatically seems morally dubious to me without more context.

It's mostly stuff specific to the application -- when the session started, last track played, why it terminated, etc -- plus a couple of very general things like whether the OS is Windows, Apple or Linux.  Nothing personally identifying. 

  • Like 1
Link to comment
Share on other sites

36 minutes ago, Innula Zenovka said:

why it terminated,

good luck with that part. That sounds rather complicated.

To get around a possible "the thing doesn't exist anymore so it can't report" logic/design error, consider as a general principal collecting data as it becomes available rather than when you want to send it. For example, perhaps your SendDataToLSL() the last track played every time you play a track and store some global LSL variables, but only forward those values on to your server with a llHTTPRequest in the case where the HUD is detached (attach event with a null key parameter). That's probably getting a bit too into the weeds of your specific application though.

 

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

18 hours ago, Quistess Alpha said:

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.

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.

Clearly, yet again, I'm not understanding something fundamental:

integer iFace = 4;

string gsMainBody;
string strMyURL;
generateMainBody(){

gsMainBody  = "<!DOCTYPE html>
<html>
   <body onload=\"SendDataToLSL()\">
   <h1>Page loaded</h1>
</body>


<script>

function SendDataToLSL() {
let text = \"Hello Avatar!\";

  var xhttp = new XMLHttpRequest();
  xhttp.open(\"POST\",\"GetSomeData\");
  xhttp.send(text);
} 
</script>

</html>";

}
default
{
    state_entry()
    {
        llClearPrimMedia(iFace);
        llRequestURL();
    }

    http_request(key id, string method, string body)
    {
        llOwnerSay("method is "+method);
        if (method == URL_REQUEST_GRANTED) {
            strMyURL = body;
            llSetPrimMediaParams(iFace,
                [PRIM_MEDIA_AUTO_PLAY, TRUE,
                 PRIM_MEDIA_CURRENT_URL, strMyURL]);
            //llSetTimerEvent(5.0);
        } else if (method == "GET") {
            generateMainBody();
            llSetContentType(id, CONTENT_TYPE_HTML);
            llHTTPResponse(id, 200, gsMainBody);
        }else if (method == "POST"){
            llOwnerSay(body);
        }
    }

} 

That loads the page (I see "Page loaded" on the face of the prim, which chats out "method is GET") but SendDataToLSL() never happens, or at least I don't hear "method is POST" or anything further from the script.  

What am I doing wrong, please?

ETA:  It seems to be to do with how I'm sending the message with AJAX.   I've adapted my earlier example, with the button, which you fixed, to use

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

<!DOCTYPE html>
<html xmlns=\"http://www.w3.org/1999/xhtml\">
<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\";
  let text = \"Hello Avatar!\";

  var xhttp = new XMLHttpRequest();
  xhttp.open(\"POST\",\"GetSomeData\");
  xhttp.send(text);
}
</script>

</body>
</html>";
}

default
{
    state_entry()
    {
        llReleaseURL(myURL);
        llRequestURL();
    }

    on_rez(integer sp){
        llResetScript();   
    }
    http_request(key id, string method, string body)
    {
        llOwnerSay("method is "+method);
        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();
            llSetContentType(id, CONTENT_TYPE_XHTML);
            llHTTPResponse(id, 200, gsMainBody);
        }
        else if (method =="POST"){
            llOwnerSay(body);
        }
    }

} 

and, when I click the button, "Hello World" appears but the http_request event doesn't fire.

Edited by Innula Zenovka
Link to comment
Share on other sites

1 hour ago, Innula Zenovka said:

and, when I click the button, "Hello World" appears but the http_request event doesn't fire.

So, the javascript does run, but because I didn't explain (or fully think through myself) how the function works, it sends a request to the wrong URL.

Apparently, when you call

xhttp.open(\"POST\",\"SomeText\");

instead of appending "SomeString" to your URL, it replaces the last path element. So, if the page the javascript is running on is:       

http://simhost-093090c972a686d4f.agni.secondlife.io:12046/cap/edc3d622-2da8-29c8-0793-9de848246b1b (Note! no trailing '/' )

it will erroneously send a request to*

http://simhost-093090c972a686d4f.agni.secondlife.io:12046/cap/SomeText

which will not get forwarded to your prim. The easiest fix is to add a trailing '/' and possibly some page-identifying text to your URL or alternatively you could embed your full url in the xhttp.open() call.

 

Quote
integer iFace = 4;

string gsMainBody;
string strMyURL;
generateMainBody(){

gsMainBody  = "
<html xmlns=\"http://www.w3.org/1999/xhtml\">
   <body onload=\"SendDataToLSL()\">
   <h1>Page loaded</h1>
   <button onclick=\"SendDataToLSL()\">Button</button>
</body>


<script>

function SendDataToLSL() {
let text = \"Hello Avatar!\";

  var xhttp = new XMLHttpRequest();
  xhttp.open(\"POST\",\"GetSomeData\");
  xhttp.send(text);
} 
</script>

</html>";

}
default
{
    state_entry()
    {
        llClearPrimMedia(iFace);
        llRequestURL();
    }

    http_request(key id, string method, string body)
    {
        llOwnerSay("method is "+method);
        llOwnerSay("path is "+llGetHTTPHeader(id,"x-path-info"));
        llOwnerSay("body is "+body+"\n");

        if (method == URL_REQUEST_GRANTED) {
            strMyURL = body;
            llSetPrimMediaParams(iFace,
                [PRIM_MEDIA_AUTO_PLAY, TRUE,
                 PRIM_MEDIA_CURRENT_URL, strMyURL+"/home"]); // specifying a page name is NOT optional in this example.
            //llSetTimerEvent(5.0);
        } else if (method == "GET") {
            generateMainBody();
            llSetContentType(id, CONTENT_TYPE_XHTML);
            llHTTPResponse(id, 200, gsMainBody);
        }else if (method == "POST"){
            llOwnerSay(body);
        }
    }
} 

 

I can't stress enough how useful it is to use xhtml, which gives you the ability to check the page in a 'real' web-browser. Even just the fact that there was no javascript error was a helpful hint.

*ETA: That is, assuming SomeText is not a fully qualified URL with "http://...."

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

1 hour ago, Innula Zenovka said:

Clearly, yet again, I'm not understanding something fundamental

Obviously I can't look inside your head to see what you do and don't know, but one thing that's kinda fundamental and I've kinda glossed over; when you request and receive a URL, you're not just getting a single URL, but an entire namespace of URLs that will forward to your prim. a request to basically anything of the form strMyURL+"/"+extra_stuff should get passed on.

  • Thanks 1
Link to comment
Share on other sites

I realised what I was doing wrong!   It needs to know where to send the message, of course.

This works:

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

<!DOCTYPE html>
<html xmlns=\"http://www.w3.org/1999/xhtml\">
<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\";
  let text = \"Hello Avatar!\";

  var xhttp = new XMLHttpRequest();
  xhttp.open(\"POST\",\""+myURL+"\");
  xhttp.send(text);
}
</script>

</body>
</html>";
}

default
{
    state_entry()
    {
        llReleaseURL(myURL);
        llRequestURL();
    }

    on_rez(integer sp){
        llResetScript();   
    }
    http_request(key id, string method, string body)
    {
        llOwnerSay("method is "+method);
        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();
            llSetContentType(id, CONTENT_TYPE_XHTML);
            llHTTPResponse(id, 200, gsMainBody);
        }
        else if (method =="POST"){
            llOwnerSay(body);
        }
    }

} 

 

  • Like 1
Link to comment
Share on other sites

Now I'm starting to get there, after climbing rather a steep learning curve with javascript.    Surprisingly, asking ChatGPT "How do I do this?" proved surprisingly effective -- it's not quite as given to wild flights of fancy with CSS and javascript as it is with LSL (and if you get bored, you can frame your questions thus: "describe, in the style of P G Wodehouse, how to make an HTTP request in Javascript" for entertaining and informative results).   The problem was I didn't understand how to reference things properly, but this seems to work

      <script>
          //<![CDATA[
        window.onload = function () {
        var myMusic = document.getElementById(\"myMusic\");
        var playButton = document.getElementById(\"play\");
        var pauseButton = document.getElementById(\"pause\");
        var skipButton = document.getElementById(\"skip\");
        var off = true;
        var on;

         // Get the icon button element
          const playlistButton = document.getElementById('playlist-button');

          // Add a click event listener to the button
          playlistButton.addEventListener('click', function() {
            let text = \"Playist\";
            var xhttp = new XMLHttpRequest();
            xhttp.open(\"POST\",\""+strURL+"\",true);
            xhttp.send(text);
          });

        skipButton.onclick =function(){
          let text = \"Skip\";
          var xhttp = new XMLHttpRequest();
          xhttp.open(\"POST\",\""+strURL+"\",true);
          xhttp.send(text);
        }
        myMusic.addEventListener(\"canplaythrough\", function() {
          duration = myMusic.duration;
        }, false);

        myMusic.addEventListener(\"ended\", function() {
          let text = \"Ended\";
          var xhttp = new XMLHttpRequest();
          xhttp.open(\"POST\",\""+strURL+"\",true);
          xhttp.send(text);
        });

        const button = document.getElementById('playPauseButton');

        button.addEventListener('click', function() {
          const icon = this.firstElementChild;
          if (icon.textContent === 'play_circle') {
            icon.textContent = 'pause_circle';
            myMusic.play();
          } else {
            myMusic.pause();
            icon.textContent = 'play_circle';
          }
        });
      };
        //]]>
      </script>

 

when combined with buttons like this

    <div class=\"icon-button-container\">
            <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0\" />
            <button id=\"playPauseButton\">
              <i class=\"material-symbols-outlined\" style=\"font-size:128px;color:SlateGrey;text-shadow:2px 2px 4px #000000 margin-right:100px;\">play_circle</i>
            </button>
            <button id = \"skip\">
              <i class=\"material-symbols-outlined\" style=\"font-size:128px;color:SlateGrey;text-shadow:2px 2px 4px #000000;\">skip_next</i>
            </button> 
            <button id=\"playlist-button\">
              <i class=\"material-symbols-outlined\" style=\"font-size:128px;color:SlateGrey;text-shadow:2px 2px 4px #000000 margin-left:100px;\">featured_play_list</i>
            </button>
      </div>

The buttons need to be big because it's on smallish HUD.   All the quote marks are escaped because it's LSL, obviously.

However, for design reasons, I may end up needing to control it using buttons on the HUD rather than as part of the shared media screen.    It's still under consideration, but if we do, is there any way to control the play/pause by touching a button that makes LSL do something?    Since play/pause is supposed to restart the track where it left off, I don't think doing anything that refreshes the screen is going to work.    

If it's not possible, we'll have to rethink some design decisions but it would good to know it it is.

 

 

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

3 hours ago, Innula Zenovka said:

is there any way to control the play/pause by touching a button that makes LSL do something?

~Possible, yes. ~practical, almost certainly not. the only way I could think to make that work would be either A) have LSL update the URL with some querystring data that gets passed into the javascript somehow,  (javascript really isn't my area of expertise, I just have good luck googling "how do I do XYZ") or, B) even worse, the javascript periodically polls the prim for "is such and such button pushed". A is better performance wise but would involve LSL guessing the amount of time the user is into the track, B is "bad" on SL servers for obvious reasons, and could have worse responsiveness.

  • Thanks 1
Link to comment
Share on other sites

  • 6 months later...

@Quistess Alpha, I was able to use your script for almost everything I need to do!

Now, I just need to add a secondary "GET" call (after the initial setup) to send more data to the page.  

Hopefully with the other examples I have, I can figure that out!

However, the catch is - I want this to be initiated by the LSL code; and I don't really want the page to "poll".    So, it's not really like the GET at startup..

Basically, this part will be used to send data to the page that will be displayed in a "table" (like a log, etc.).  I suppose it will be additional XHTML on the page/prim/MOAP side, to intercept and interpret the call.

Thanks,

Love

 

Link to comment
Share on other sites

1 hour ago, Love Zhaoying said:

However, the catch is - I want this to be initiated by the LSL code; and I don't really want the page to "poll".    So, it's not really like the GET at startup..

The sanest way I've found to have LSL 'do something' to MOAP is to make it reload the page (set the media URL to something superficially different*) and send an updated page after receiving the new request.

ETA: * exactly what things work for acchieving that vary minutely depending on the script, I think the "most correct" method is to add some unused parameter to the query string of the URL (url+"?value="+(string)(++global_variable)), but for simple cases just adding a new number at the end works too (see my "TV" script in the library).

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 308 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...