Master the art of media streaming in virtual worlds. Learn to create interactive radio and TV stations, manage broadcasts, and engage your audience with professional entertainment systems.
Broadcasting in virtual worlds opens up incredible opportunities for entertainment, community building, and creative expression. This course will teach you everything you need to create professional radio and TV stations.
Before you start building, it's crucial to understand the two different ways media can be streamed in virtual worlds:
What it is: A single audio stream that plays across an entire parcel of land.
How it works: Set by the land owner in the parcel settings. Every avatar on that parcel hears the same stream.
Best for: Background music for clubs, stores, or theme parks. Ambient soundscapes.
Control: Land owner only. Cannot be changed by objects or visitors.
What it is: Individual objects that can display audio, video, or web pages on their faces.
How it works: Set via LSL scripts or manually. Each prim can have different media.
Best for: Interactive radios, TVs, billboards, kiosks, art installations.
Control: Can be scripted for interactive user control. Supports multiple streams.
Setting up parcel audio is the simplest way to add background music to your land.
You need a direct URL to an audio stream. Look for URLs ending in formats like .mp3, .m3u, or port numbers like :8000. Free options include SHOUTcast directories and royalty-free music streams.
Right-click the ground on your parcel and select About Land. Go to the Sound tab.
In the "Music URL" field, paste your stream URL. Click Set, then OK.
Make sure your viewer's media is enabled. You should see a play button appear in your viewer's media controls. Click it to start the stream.
Not all URLs work as streams. Here's what to look for:
| Valid Stream Type | Example | Notes |
|---|---|---|
| Direct MP3 Stream | http://server.com:8000/stream |
Most common for internet radio |
| M3U Playlist | http://server.com/stream.m3u |
Contains stream URL inside |
| PLS Playlist | http://server.com/stream.pls |
Alternative playlist format |
| Direct Audio File | http://server.com/audio.mp3 |
Works but will loop or stop |
Media on a prim allows you to create interactive media players. The key LSL function is llSetPrimMediaParams(), which controls what displays on a prim's face.
Let's build your first interactive radio player. When touched, it will toggle between playing a stream and being off.
// Simple On/Off Radio Player
// Touch to toggle between playing and stopped
// --- CONFIGURATION ---
string RADIO_STREAM_URL = "http://streaming.radionomy.com/ClassicalMusic";
string OFF_IMAGE = "https://alifevirtual.com/images/radio_off.png";
// --- STATE ---
integer isPlaying = FALSE;
default
{
state_entry()
{
// Start with radio off
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, OFF_IMAGE,
PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_NONE
]);
llSetText("Radio OFF\nTouch to Play", <1,1,1>, 1.0);
}
touch_start(integer num)
{
if (isPlaying == FALSE)
{
// Turn ON: Play the stream
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, RADIO_STREAM_URL,
PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_STANDARD,
PRIM_MEDIA_AUTO_PLAY, TRUE
]);
llSetText("Radio ON\nTouch to Stop", <0,1,0>, 1.0);
llSay(0, "♪ Radio started ♪");
isPlaying = TRUE;
}
else
{
// Turn OFF: Show off image
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, OFF_IMAGE,
PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_NONE
]);
llSetText("Radio OFF\nTouch to Play", <1,1,1>, 1.0);
llSay(0, "Radio stopped.");
isPlaying = FALSE;
}
}
}
state_entry() runs when the script starts, setting the radio to "off"touch_start() runs when someone touches the primisPlaying variable tracks whether the radio is on or offllSetPrimMediaParams() to change the media URLRez a cube prim in a sandbox or on your land.
Right-click the cube, select Edit, go to the Content tab, click New Script, and paste the radio script.
Make sure media streaming is enabled in your viewer preferences (Preferences → Sound & Media).
Touch the cube. The hover text should change to "Radio ON" and you should hear music!
A real radio has multiple stations. We'll use LSL lists to store station data and linked prims for channel buttons.
Create a main cube prim for the radio housing.
Create two smaller prims for "Next" and "Previous" buttons. Position them on the radio body.
Select the buttons first (Shift+click), then select the main body last. Press Ctrl+L to link them. The main body becomes the root prim.
// Multi-Station Radio Controller
// Place in ROOT PRIM
// --- CONFIGURATION ---
list stationNames = [
"Classical Relaxation",
"Jazz Lounge",
"Rock Classics",
"Electronic Beats",
"World Music"
];
list stationURLs = [
"http://stream1.example.com:8000",
"http://stream2.example.com:8000",
"http://stream3.example.com:8000",
"http://stream4.example.com:8000",
"http://stream5.example.com:8000"
];
string OFF_IMAGE = "https://alifevirtual.com/images/radio_off.png";
// --- STATE ---
integer currentStation = 0;
integer isPlaying = FALSE;
// --- FUNCTIONS ---
updateDisplay()
{
if (isPlaying)
{
string url = llList2String(stationURLs, currentStation);
string name = llList2String(stationNames, currentStation);
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, url,
PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_STANDARD,
PRIM_MEDIA_AUTO_PLAY, TRUE
]);
llSetText("♪ NOW PLAYING ♪\n" + name +
"\n[" + (string)(currentStation + 1) + "/" +
(string)llGetListLength(stationNames) + "]",
<0,1,0>, 1.0);
}
else
{
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, OFF_IMAGE,
PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_NONE
]);
llSetText("Radio OFF\nTouch to Power On\nButtons: Next/Prev Station",
<1,1,1>, 1.0);
}
}
changeStation(integer direction)
{
integer numStations = llGetListLength(stationURLs);
currentStation += direction;
// Wrap around
if (currentStation >= numStations)
currentStation = 0;
else if (currentStation < 0)
currentStation = numStations - 1;
if (isPlaying)
{
string name = llList2String(stationNames, currentStation);
llSay(0, "Switching to: " + name);
updateDisplay();
}
}
default
{
state_entry()
{
// Name the button prims for identification
llSetLinkPrimitiveParamsFast(2, [PRIM_NAME, "NextButton"]);
llSetLinkPrimitiveParamsFast(3, [PRIM_NAME, "PrevButton"]);
isPlaying = FALSE;
updateDisplay();
}
touch_start(integer num)
{
// Only respond to touches on the root prim (link 1)
if (llDetectedLinkNumber(0) == 1)
{
isPlaying = !isPlaying; // Toggle power
if (isPlaying)
llSay(0, "Radio powered ON");
else
llSay(0, "Radio powered OFF");
updateDisplay();
}
}
link_message(integer sender, integer num, string str, key id)
{
// Messages from button prims
if (str == "next")
{
changeStation(1); // Next station
}
else if (str == "prev")
{
changeStation(-1); // Previous station
}
}
}
// Radio Button Script
// Place in BOTH button prims
// Detects which button is touched and sends appropriate message
default
{
touch_start(integer num)
{
string myName = llGetObjectName();
if (myName == "NextButton")
{
llMessageLinked(LINK_ROOT, 0, "next", NULL_KEY);
}
else if (myName == "PrevButton")
{
llMessageLinked(LINK_ROOT, 0, "prev", NULL_KEY);
}
}
}
TV stations work almost identically to radio—the main difference is the media type. Instead of audio streams, you provide video URLs.
// Simple Looping Movie Screen
// Plays a video on continuous loop
// --- CONFIGURATION ---
string MOVIE_URL = "https://archive.org/download/night_of_the_living_dead/night_of_the_living_dead_512kb.mp4";
string MOVIE_TITLE = "Night of the Living Dead";
default
{
state_entry()
{
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, MOVIE_URL,
PRIM_MEDIA_WIDTH_PIXELS, 1280,
PRIM_MEDIA_HEIGHT_PIXELS, 720,
PRIM_MEDIA_AUTO_PLAY, TRUE,
PRIM_MEDIA_AUTO_LOOP, TRUE,
PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_MINI
]);
llSetText("NOW SHOWING\n" + MOVIE_TITLE + "\n(Public Domain Film)",
<1,1,0>, 1.0);
}
}
// Multi-Channel TV System
// Touch to cycle through channels
list channelNames = [
"News Network",
"Movies 24/7",
"Music Videos",
"Educational"
];
list channelURLs = [
"https://example.com/news-live.m3u8",
"https://example.com/movies.mp4",
"https://example.com/music-stream",
"https://example.com/education.mp4"
];
integer currentChannel = 0;
integer isPowered = TRUE;
updateTV()
{
if (isPowered)
{
string url = llList2String(channelURLs, currentChannel);
string name = llList2String(channelNames, currentChannel);
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, url,
PRIM_MEDIA_WIDTH_PIXELS, 1920,
PRIM_MEDIA_HEIGHT_PIXELS, 1080,
PRIM_MEDIA_AUTO_PLAY, TRUE,
PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_STANDARD
]);
llSetText("Channel " + (string)(currentChannel + 1) +
": " + name, <0,1,1>, 1.0);
}
else
{
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, "",
PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_NONE
]);
llSetText("TV OFF", <0.5,0.5,0.5>, 1.0);
}
}
default
{
state_entry()
{
updateTV();
}
touch_start(integer num)
{
key toucher = llDetectedKey(0);
string message = llDetectedTouchST(0) < 0.5 ? "Power toggled" : "Channel changed";
// Left side: power toggle
if (llDetectedTouchST(0) < 0.3)
{
isPowered = !isPowered;
updateTV();
llInstantMessage(toucher, "TV " + (isPowered ? "ON" : "OFF"));
}
// Right side: next channel
else if (llDetectedTouchST(0) > 0.7)
{
if (isPowered)
{
currentChannel++;
if (currentChannel >= llGetListLength(channelURLs))
currentChannel = 0;
updateTV();
llInstantMessage(toucher, "Channel: " +
llList2String(channelNames, currentChannel));
}
}
// Center: show info
else
{
if (isPowered)
{
llInstantMessage(toucher, "Now playing: " +
llList2String(channelNames, currentChannel) +
"\nChannel " + (string)(currentChannel + 1) +
" of " + (string)llGetListLength(channelNames));
}
}
}
}
llDetectedTouchST() to detect WHERE on the prim was touched. Values range from 0.0 (left) to 1.0 (right), allowing you to create invisible button zones!
Without access control, anyone can change your radio station or TV channel. For public entertainment venues, clubs, or stores, you need to restrict who can operate your media systems.
// Owner-Only Radio Control
// Only the object owner can operate the radio
string RADIO_URL = "http://yourstream.com:8000";
integer isPlaying = FALSE;
default
{
state_entry()
{
llSetText("Owner-Only Radio\nTouch to Control", <1,1,1>, 1.0);
}
touch_start(integer num)
{
key toucher = llDetectedKey(0);
// Check if toucher is the owner
if (toucher == llGetOwner())
{
isPlaying = !isPlaying;
if (isPlaying)
{
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, RADIO_URL,
PRIM_MEDIA_AUTO_PLAY, TRUE
]);
llOwnerSay("Radio started");
}
else
{
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, ""
]);
llOwnerSay("Radio stopped");
}
}
else
{
// Not the owner - deny access
llInstantMessage(toucher,
"Sorry, only the owner can operate this radio.");
}
}
}
Perfect for clubs, businesses, or team environments where multiple people need access.
Edit the object, go to the General tab, click Set next to "Group", and choose your group.
Check the Share checkbox to activate group permissions.
In your script, use llDetectedGroup(0) to check if the toucher is in the same group.
// Group-Only Radio Control
// Any member of the object's group can operate it
string RADIO_URL = "http://yourstream.com:8000";
integer isPlaying = FALSE;
default
{
state_entry()
{
llSetText("Group Radio\nGroup Members Can Control", <1,1,1>, 1.0);
}
touch_start(integer num)
{
key toucher = llDetectedKey(0);
// Check if toucher is in the same group as the object
if (llDetectedGroup(0))
{
isPlaying = !isPlaying;
if (isPlaying)
{
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, RADIO_URL,
PRIM_MEDIA_AUTO_PLAY, TRUE,
PRIM_MEDIA_CONTROLS, PRIM_MEDIA_CONTROLS_STANDARD
]);
llSay(0, "Radio started by " + llDetectedName(0));
}
else
{
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, ""
]);
llSay(0, "Radio stopped by " + llDetectedName(0));
}
}
else
{
llInstantMessage(toucher,
"Sorry, only group members can operate this radio.");
}
}
}
For precise control, maintain a list of authorized user keys.
// Whitelist-Based Access Control
// Only specific avatars can operate the system
list authorizedUsers = [
"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", // Replace with actual UUIDs
"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"cccccccc-cccc-cccc-cccc-cccccccccccc"
];
string RADIO_URL = "http://yourstream.com:8000";
integer isPlaying = FALSE;
integer isAuthorized(key user)
{
return (llListFindList(authorizedUsers, [(string)user]) != -1)
|| (user == llGetOwner());
}
default
{
state_entry()
{
llSetText("Private Radio\nAuthorized Users Only", <1,0.5,0>, 1.0);
}
touch_start(integer num)
{
key toucher = llDetectedKey(0);
if (isAuthorized(toucher))
{
isPlaying = !isPlaying;
if (isPlaying)
{
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, RADIO_URL,
PRIM_MEDIA_AUTO_PLAY, TRUE
]);
llOwnerSay("Radio started by: " + llDetectedName(0));
}
else
{
llSetPrimMediaParams(0, [PRIM_MEDIA_CURRENT_URL, ""]);
llOwnerSay("Radio stopped by: " + llDetectedName(0));
}
}
else
{
llInstantMessage(toucher, "Access denied. You are not authorized.");
llOwnerSay("Unauthorized access attempt by: " +
llDetectedName(0) + " (" + (string)toucher + ")");
}
}
}
Good user experience separates amateur projects from professional installations.
// Example: Comprehensive Status Display
updateStatus(string status, string details, vector color)
{
llSetText("═══ RADIO STATUS ═══\n" +
status + "\n" +
details + "\n" +
"══════════════════",
color, 1.0);
}
// Usage examples:
updateStatus("OFFLINE", "Touch to power on", <0.5,0.5,0.5>);
updateStatus("PLAYING", "Jazz FM 24/7", <0,1,0>);
updateStatus("BUFFERING", "Please wait...", <1,1,0>);
// Example: Immediate Feedback
touch_start(integer num)
{
llSetText("⟳ Processing...", <1,1,0>, 1.0); // Immediate feedback
// Process the action
isPlaying = !isPlaying;
if (isPlaying)
{
llSetPrimMediaParams(0, [PRIM_MEDIA_CURRENT_URL, RADIO_URL]);
llSetText("♪ NOW PLAYING ♪\n" + stationName, <0,1,0>, 1.0);
llPlaySound("power_on", 1.0); // Audio feedback
}
else
{
llSetPrimMediaParams(0, [PRIM_MEDIA_CURRENT_URL, ""]);
llSetText("Radio OFF\nTouch to Play", <1,1,1>, 1.0);
llPlaySound("power_off", 1.0);
}
}
// Example: Graceful Error Handling
integer playStream(string url, string name)
{
if (url == "")
{
llOwnerSay("ERROR: Empty stream URL for station: " + name);
llSetText("⚠ ERROR ⚠\nInvalid Stream", <1,0,0>, 1.0);
return FALSE;
}
llSetPrimMediaParams(0, [
PRIM_MEDIA_CURRENT_URL, url,
PRIM_MEDIA_AUTO_PLAY, TRUE
]);
llSetText("♪ " + name + " ♪", <0,1,0>, 1.0);
return TRUE;
}
llGetListLength() repeatedlyllOwnerSay() or llInstantMessage() for feedback// Example: Chat Commands for Accessibility
listen(integer channel, string name, key id, string msg)
{
msg = llToLower(llStringTrim(msg, STRING_TRIM));
if (msg == "help" || msg == "?")
{
llInstantMessage(id,
"═══ RADIO COMMANDS ═══\n" +
"on - Power on radio\n" +
"off - Power off radio\n" +
"next - Next station\n" +
"prev - Previous station\n" +
"list - Show all stations\n" +
"status - Current status\n" +
"volume [0-100] - Set volume");
}
else if (msg == "list")
{
integer i;
string stations = "═══ STATIONS ═══\n";
for (i = 0; i < llGetListLength(stationNames); i++)
{
stations += (string)(i + 1) + ". " +
llList2String(stationNames, i) + "\n";
}
llInstantMessage(id, stations);
}
// ... more commands
}
Broadcasting copyrighted content without permission is illegal and can result in serious consequences.
| Resource | Type | License |
|---|---|---|
| Internet Archive | Audio, Video, Movies | Public Domain, Creative Commons |
| Free Music Archive | Music | Various Creative Commons |
| SHOUTcast Directory | Internet Radio Streams | Varies by station |
| Incompetech | Music | Royalty-Free (attribution) |
| Pexels Videos | Video Content | Free for commercial use |
Task: Find a legal internet radio stream and set it as your parcel's background music.
Steps:
Bonus: Create a sign informing visitors about the music and how to enable it.
Task: Create a working on/off radio using the simple radio script from Lesson 2.
Requirements:
Task: Build a complete multi-station radio with at least 3 stations.
Requirements:
Task: Set up a large screen that plays a public domain movie on loop.
Requirements:
Task: Create a radio that only group members can control.
Requirements:
Task: Create a complete DJ booth with multiple features.
Requirements:
Bonus: Add a tip jar for donations!
Create a comprehensive media entertainment center that combines everything you've learned:
You've completed the Radio and TV Station Creation course!
You now have the skills to create professional media entertainment systems in virtual worlds. You understand streaming technology, LSL scripting for media control, access management, and legal considerations.