Sorin Todys - Advanced expert with 20+ years of experience in virtual worlds
All classes take place in Alife Virtual World at our dedicated Alife Virtual School region
Learn and Grow at Alife Virtual World School
Course Code: ART-504 | The School of Digital Arts
Difficulty Level: Advanced
Lead Instructor: Sorin Todys
Welcome to one of the most exciting and dynamic courses offered at Alife Virtual School! In ART-504, we move beyond static builds and into the world of live, streaming media. Have you ever dreamed of running your own radio station, hosting a talk show, DJing at a virtual club, or creating a movie theater for your friends? This course will give you the technical foundation and scripting knowledge to make it a reality.
Broadcasting media is the lifeblood of social spaces in virtual worlds. It sets the mood, delivers information, and provides entertainment. By learning to control it, you are learning to craft immersive experiences. This advanced course, led by our Linden Scripting Language (LSL) expert, Sorin Todys, will guide you through every step, from the underlying theory of media streams to scripting complex, user-friendly devices.
Upon successful completion of this course, you will be able to:
This isn't just about copying a script. By the end, you will have mastered the art and science of in-world media broadcasting. You'll be able to:
llSetPrimMediaParams, llGetPrimMediaParams, and llSetParcelMusicURL, and learn to combine them with touch events and data storage for powerful results.This is an Advanced course. To succeed, students should have a solid foundation in the following areas:
touch_start), and basic state management. You should be able to create, save, and add a script to a prim. (Recommended: ART-401)Before we can control media, we must understand what it is. In Alife Virtual, all audio and video are "streams." This means the data isn't stored in the virtual world itself; instead, our viewer (Firestorm) is directed to an external internet address (a URL) to play the content. Think of it like your web browser playing a YouTube video—the video isn't on your computer, it's streaming from YouTube's servers.
There are two primary ways to deliver this media to users in-world:
llSetParcelMusicURL().llSetPrimMediaParams()..mp3, .m3u, or a port number (e.g., http://stream.example.com:8000/stream). For video, it could be a direct link to a .mp4 file. Finding these direct URLs is often the hardest part!
Let's start with the easiest method: setting the music for your land. This requires you to have rights to modify the parcel.
http://dradio.org:8000/7 (This is a fictional example URL; you'll need to find a real one).World > Parcel Details.http://dradio.org:8000/7.Now, let's display a webpage on a prim face. This uses the same principle as a TV, but is simpler to start with.
https://alifevirtual.com into the URL field and click "Apply".You've just done manually what we're about to do with a script! The front face of your prim should now display the Alife Virtual website. You can click on it and interact with it like a mini-browser. This is the core of MOAP.
In Lesson 1, we set media manually. That's fine for a static display, but a real radio or TV needs to be interactive. It needs to be turned on and off, have its channels changed, and respond to users. This is where LSL comes in. We will use the touch_start event to trigger changes to the prim's media parameters.
The key function is llSetPrimMediaParams. It's a versatile function that takes a list of parameters to control the media on a prim. The most basic parameter is PRIM_MEDIA_CURRENT_URL, which sets the webpage or stream to display.
Let's create a simple radio that you can touch to turn on and touch again to turn off. This introduces the concept of a "state machine" in your script, which keeps track of whether the radio is currently on or off.
Create a new script inside a prim and paste the following code:
// Simple On/Off Radio Script
// By Sorin Todys, Alife Virtual School
// --- CONFIGURATION ---
// Paste your radio stream URL here
string RADIO_STREAM_URL = "http://dradio.org:8000/7";
// This is the URL shown when the radio is "off"
string OFF_URL = "https://alifevirtual.com/images/off_air.png";
// --- SCRIPT STATE ---
integer is_playing = FALSE; // State variable: FALSE means off, TRUE means on
default
{
state_entry()
{
// When the script starts, turn the radio "off"
// We do this by setting the media to our "off" image
llSetPrimMediaParams(0, [PRIM_MEDIA_CURRENT_URL, OFF_URL, PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_NONE]);
llSetText("Radio is OFF\nTouch to Play", <1,1,1>, 1.0);
}
touch_start(integer total_number)
{
// This event fires when someone touches the prim
if (is_playing == FALSE)
{
// If the radio is currently off, turn it ON
llSay(0, "Starting radio stream...");
// Set the media URL to our radio stream
llSetPrimMediaParams(0, [PRIM_MEDIA_CURRENT_URL, RADIO_STREAM_URL, PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_STANDARD]);
llSetText("Radio is ON\nTouch to Stop", <0,1,0>, 1.0);
is_playing = TRUE; // Update the state
}
else
{
// If the radio is currently on, turn it OFF
llSay(0, "Stopping radio stream.");
// Set the media URL back to our "off" image
llSetPrimMediaParams(0, [PRIM_MEDIA_CURRENT_URL, OFF_URL, PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_NONE]);
llSetText("Radio is OFF\nTouch to Play", <1,1,1>, 1.0);
is_playing = FALSE; // Update the state
}
}
}
An on/off switch is good, but a real radio has multiple stations. To do this, we'll store our station URLs in an LSL list. We'll also need a variable to keep track of the current station index. For this, we'll need to use linked prims for the "Next" and "Previous" buttons.
Setup:
Ctrl+L. The main body is now the "root" prim.// Multi-Station Radio Script
// By Sorin Todys, Alife Virtual School
// --- CONFIGURATION ---
list g_station_urls; // Global list to hold our station URLs
list g_station_names; // Global list to hold station names
// --- SCRIPT STATE ---
integer g_current_station_index = 0; // Index for the current station
integer g_is_playing = FALSE; // Radio on/off state
// --- HELPER FUNCTION ---
// This function updates the media and hover text
update_media()
{
if (g_is_playing)
{
string url = llList2String(g_station_urls, g_current_station_index);
string name = llList2String(g_station_names, g_current_station_index);
llSetPrimMediaParams(0, [PRIM_MEDIA_CURRENT_URL, url, PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_STANDARD]);
llSetText("Now Playing:\n" + name + "\n(Touch main body to turn OFF)", <0,1,0>, 1.0);
}
else
{
// When off, we can show an image or a blank page
llSetPrimMediaParams(0, [PRIM_MEDIA_CURRENT_URL, "", PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_NONE]);
llSetText("Radio is OFF\n(Touch main body to turn ON)", <1,1,1>, 1.0);
}
}
default
{
state_entry()
{
// --- POPULATE OUR STATION LISTS ---
g_station_names = ["Classical Calm", "Rock Anthems", "Jazz Club"];
g_station_urls = ["http://dradio.org:8000/1", "http://dradio.org:8000/2", "http://dradio.org:8000/3"]; // Replace with real URLs!
// Set the link names for our buttons for easy identification
llSetLinkPrimitiveParamsFast(2, [PRIM_NAME, "NextButton"]);
llSetLinkPrimitiveParamsFast(3, [PRIM_NAME, "PrevButton"]);
g_is_playing = FALSE;
update_media();
}
link_message(integer sender_num, integer num, string str, key id)
{
// This event listens for messages from other prims in the linkset
if (str == "touch")
{
// Check which linked prim was touched by its name
string prim_name = llGetLinkName(num);
if (prim_name == "NextButton")
{
g_current_station_index++;
// Loop back to the beginning if we go past the end of the list
if (g_current_station_index >= llGetListLength(g_station_urls))
{
g_current_station_index = 0;
}
llSay(0, "Next station...");
update_media();
}
else if (prim_name == "PrevButton")
{
g_current_station_index--;
// Loop to the end if we go past the beginning
if (g_current_station_index < 0)
{
g_current_station_index = llGetListLength(g_station_urls) - 1;
}
llSay(0, "Previous station...");
update_media();
}
}
}
touch_start(integer total_number)
{
// This event only fires for the root prim
// We use this for the ON/OFF toggle
if (llDetectedLinkNumber(0) == 1) // Ensure it's the root prim being touched
{
g_is_playing = !g_is_playing; // This is a cool shortcut to toggle a boolean
update_media();
}
}
}
// --- SCRIPT FOR BUTTONS ---
// Place this tiny script in EACH of the button prims (Next and Previous)
default
{
touch_start(integer total_number)
{
// When touched, send a message to the root prim
llMessageLinked(LINK_ROOT, 0, "touch", NULL_KEY);
}
}
You might be surprised to learn that a TV is functionally identical to the radio we just built. The only difference is the media URL and a few extra parameters. Instead of an audio stream, you provide a video stream URL. This could be a link to a .mp4 file or a live video feed.
The llSetPrimMediaParams function can take many more arguments to control the video playback. Here are some of the most useful:
PRIM_MEDIA_WIDTH and PRIM_MEDIA_HEIGHT: Integers that set the resolution of the media. This helps prevent distortion.PRIM_MEDIA_AUTO_PLAY: Set to TRUE to make the video play automatically.PRIM_MEDIA_AUTO_LOOP: Set to TRUE to make the video loop when it finishes.PRIM_MEDIA_FIRST_CLICK_INTERACT: Set to TRUE to allow users to click through the video to the webpage it's on.This script will play a public domain movie on a loop. We'll use a URL from the Internet Archive.
// Simple Looping Movie Player
// By Sorin Todys, Alife Virtual School
// Public domain movie: "Night of the Living Dead" from archive.org
string MOVIE_URL = "http://www.archive.org/download/night_of_the_living_dead/night_of_the_living_dead_512kb.mp4";
default
{
state_entry()
{
// Set the media on face 0.
// We specify resolution, auto play, and looping.
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, MOVIE_URL,
PRIM_MEDIA_WIDTH, 640,
PRIM_MEDIA_HEIGHT, 480,
PRIM_MEDIA_AUTO_PLAY, TRUE,
PRIM_MEDIA_AUTO_LOOP, TRUE,
PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_MINI
]);
llSetText("Now Showing:\nNight of the Living Dead", <1,1,0>, 1.0);
}
}
Your media player is amazing, but what if you don't want just anyone changing the station at your club? You need to add security. We can easily modify our touch events to check who is clicking the prim.
To do this, we'll use llDetectedKey(0) to get the unique key of the avatar who touched the prim, and llGetOwner() to get the key of the object's owner. We can also check if the user is in the same group as the object.
Let's modify the touch_start event from our simple On/Off radio to be owner-only.
touch_start(integer total_number)
{
// Check if the person who touched is the owner of the object
if (llDetectedKey(0) == llGetOwner())
{
// All the on/off logic from before goes inside this 'if' block
if (is_playing == FALSE)
{
// ... turn on radio ...
is_playing = TRUE;
}
else
{
// ... turn off radio ...
is_playing = FALSE;
}
}
else
{
// If it's not the owner, send them a message
llInstantMessage(llDetectedKey(0), "Sorry, only the owner can operate this radio.");
}
}
This is extremely useful for clubs and businesses. First, you must set the object to the correct group and "Share" it. Then, use llDetectedGroup(0).
touch_start(integer total_number)
{
// llDetectedGroup returns TRUE if the toucher is in the same group as the prim
if (llDetectedGroup(0) == TRUE)
{
// ... all your radio logic here ...
}
else
{
llInstantMessage(llDetectedKey(0), "Sorry, only group members can operate this radio.");
}
}
llSetText to show the current status (On/Off, station name). Use llSay or llInstantMessage to confirm actions. A silent, unresponsive object is confusing.state_entry.Time to put your knowledge into practice. These exercises are designed to be completed on your own land or in a sandbox area where you have permission to rez and run scripts.
World > Parcel Details > Sound, and set the stream. Invite