Create Your Radio or TV Station

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.


Course Code
ART-504

Level
Advanced

Duration
4-6 Hours

Certificate
Available

Course Overview

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.

What You'll Learn

  • Understanding parcel audio vs. media-on-a-prim streaming
  • Setting up and managing internet radio streams
  • Creating interactive radio players with LSL scripting
  • Building multi-station radio systems
  • Setting up TV stations and video streaming
  • Implementing access control and security
  • Professional UX design for media systems
  • Legal considerations and content licensing

Prerequisites

  • Basic understanding of LSL scripting (variables, events, functions)
  • Ability to create and edit prims
  • Understanding of object permissions
  • Access to land or a sandbox for testing

What You'll Need

  • A reliable internet radio or video stream URL
  • Basic image editing software (optional, for custom textures)
  • Your own land or sandbox access

Lesson 1: Understanding Media Streaming

Two Types of Media in Virtual Worlds

Before you start building, it's crucial to understand the two different ways media can be streamed in virtual worlds:

Parcel Audio

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.

Media on a Prim

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.

Parcel Audio Setup (No Scripting Required)

Setting up parcel audio is the simplest way to add background music to your land.

1 Find a Stream URL

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.

2 Open Parcel Settings

Right-click the ground on your parcel and select About Land. Go to the Sound tab.

3 Enter Stream URL

In the "Music URL" field, paste your stream URL. Click Set, then OK.

4 Test It

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.

Pro Tip: Visitors must manually enable parcel audio in their preferences. Consider placing a sign near your entrance informing visitors about your parcel music and how to enable it.

Understanding Stream URLs

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
Important: Many music streaming services (Spotify, Apple Music, etc.) do NOT provide direct stream URLs and will not work. You need actual internet radio streams or self-hosted audio.

Lesson 2: Scripting Your First Radio

Media on a Prim Basics

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.

Key Concepts

  • Prim Faces: Each side of a prim is a "face" (numbered 0-5 for a cube). You set media per face.
  • Media URL: The URL of your stream, image, or webpage.
  • Media Controls: Whether users see play/pause buttons.
  • Auto-play: Whether media starts automatically.

Simple On/Off Radio

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;
        }
    }
}
How It Works:
  • state_entry() runs when the script starts, setting the radio to "off"
  • touch_start() runs when someone touches the prim
  • The isPlaying variable tracks whether the radio is on or off
  • We use llSetPrimMediaParams() to change the media URL
  • Face 0 is the first face of the prim

Testing Your Radio

1 Create a Cube

Rez a cube prim in a sandbox or on your land.

2 Add the Script

Right-click the cube, select Edit, go to the Content tab, click New Script, and paste the radio script.

3 Enable Media

Make sure media streaming is enabled in your viewer preferences (Preferences → Sound & Media).

4 Touch to Test

Touch the cube. The hover text should change to "Radio ON" and you should hear music!

Lesson 3: Multi-Station Radio System

Building a Professional Radio

A real radio has multiple stations. We'll use LSL lists to store station data and linked prims for channel buttons.

Multi-Prim Setup

1 Create the Radio Body

Create a main cube prim for the radio housing.

2 Add Control Buttons

Create two smaller prims for "Next" and "Previous" buttons. Position them on the radio body.

3 Link the Objects

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.

Main Controller Script (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
        }
    }
}

Button Script (Place in Each Button)

// 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);
        }
    }
}
How It Works:
  • The main script stores stations in two parallel lists (names and URLs)
  • Touch the main body to power on/off
  • Touch buttons to change stations
  • Buttons send linked messages to the root prim
  • The display updates automatically to show current station

Lesson 4: Creating a TV Station

Video Streaming Basics

TV stations work almost identically to radio—the main difference is the media type. Instead of audio streams, you provide video URLs.

Supported Video Formats

  • MP4 (H.264): Most widely supported
  • WebM: Good for web-based content
  • Live Streams: HLS (.m3u8) and DASH formats
  • YouTube/Vimeo: Embed URLs (may have limitations)

Simple Movie Screen

// 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);
    }
}

Interactive TV with Multiple Channels

// 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));
            }
        }
    }
}
Advanced Touch Detection: This script uses 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!

Lesson 5: Security & Access Control

Why Security Matters

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 Control

// 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.");
        }
    }
}

Group-Based Control

Perfect for clubs, businesses, or team environments where multiple people need access.

1 Set Object to Group

Edit the object, go to the General tab, click Set next to "Group", and choose your group.

2 Enable Group Sharing

Check the Share checkbox to activate group permissions.

3 Use Group Detection

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.");
        }
    }
}

Whitelist System (Advanced)

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 + ")");
        }
    }
}

Lesson 6: UX Best Practices

Creating Professional Media Experiences

Good user experience separates amateur projects from professional installations.

1. Clear Visual Feedback

// 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>);

2. Informative Hover Text

  • Always show current state: ON/OFF, playing/stopped
  • Include instructions: "Touch to play", "Left-click: power, Right-click: next"
  • Display current content: Station name, channel, song title
  • Use color coding: Green for active, red for off, yellow for loading

3. Responsive Controls

// 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);
    }
}

4. Error Handling

// 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;
}

5. Performance Considerations

  • Cache frequently used values: Don't call llGetListLength() repeatedly
  • Use timers wisely: Don't update displays unnecessarily
  • Limit chat spam: Use llOwnerSay() or llInstantMessage() for feedback
  • Clean up listeners: Remove listeners when not needed

6. Accessibility Features

// 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
}

Lesson 7: Legal Considerations & Content Licensing

Understanding Copyright

Broadcasting copyrighted content without permission is illegal and can result in serious consequences.

Critical Warning: Just because you can stream something doesn't mean you legally can. Always verify you have the rights to broadcast content publicly.

Safe Content Sources

✅ Legal Options

  • Public Domain: Content where copyright has expired or never existed
  • Creative Commons: Content licensed for sharing (check specific license terms)
  • Royalty-Free Music Services: Services like epidemic sound, AudioJungle
  • Internet Radio Stations: Many provide embeddable streams legally
  • Your Own Content: Music, videos, or shows you created yourself
  • Licensed Streams: Services where you pay for broadcasting rights

❌ Avoid These

  • Ripping audio from commercial music streaming services
  • Broadcasting commercial radio stations without permission
  • Using copyrighted movies or TV shows
  • Streaming sports events or pay-per-view content
  • Using music from YouTube without explicit permission

Resources for Legal Content

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

Best Practices

  • Document your sources: Keep records of where you got content and license terms
  • Provide attribution: When required, credit the creator in your display text
  • Read licenses carefully: Some "free" content requires attribution or prohibits commercial use
  • When in doubt, ask: Contact the content creator for permission
  • Consider creating original content: Make your own music or videos

Hands-On Exercises

Exercise 1: Set Up Parcel Audio

Task: Find a legal internet radio stream and set it as your parcel's background music.

Steps:

  1. Search for "free internet radio stream" or use a SHOUTcast directory
  2. Copy the stream URL (should end in :8000 or similar)
  3. Set it in your parcel's About Land → Sound tab
  4. Test by enabling media in your viewer

Bonus: Create a sign informing visitors about the music and how to enable it.

Exercise 2: Build a Simple Radio

Task: Create a working on/off radio using the simple radio script from Lesson 2.

Requirements:

  • Radio must toggle on/off when touched
  • Display appropriate status text
  • Use a valid stream URL
  • Add custom textures or colors to make it look like a radio

Exercise 3: Multi-Station Radio

Task: Build a complete multi-station radio with at least 3 stations.

Requirements:

  • Main body with power control
  • Next and Previous buttons
  • At least 3 different stations
  • Display shows current station name and number
  • Professional appearance with proper textures

Exercise 4: Create a Movie Screen

Task: Set up a large screen that plays a public domain movie on loop.

Requirements:

  • Use a large, flat prim (at least 4x3 meters)
  • Find a legal video URL (try Internet Archive)
  • Configure for auto-play and loop
  • Add seating or a viewing area
  • Include a sign with movie title and credits

Exercise 5: Secured Group Radio

Task: Create a radio that only group members can control.

Requirements:

  • Set object to a group and enable sharing
  • Implement group-based access control
  • Send appropriate denial messages to non-members
  • Log all control actions

Exercise 6: Professional DJ Booth

Task: Create a complete DJ booth with multiple features.

Requirements:

  • At least 5 stations/playlists
  • Visual display showing now playing
  • Chat commands for controlling playback
  • Access control (owner or group only)
  • Professional build quality with textures and effects
  • Particle effects when music is playing

Bonus: Add a tip jar for donations!

Final Project: Complete Media Entertainment System

Project Requirements

Create a comprehensive media entertainment center that combines everything you've learned:

Core Features (Required)

  • Multi-mode operation: Radio mode and TV mode
  • Multiple channels: At least 5 radio stations and 3 video channels
  • Interactive controls: Power, mode switch, channel up/down
  • Security: Group-based or whitelist access control
  • Professional UX: Clear status displays, feedback, error handling
  • Legal content: Only use properly licensed media

Advanced Features (Choose 3+)

  • Chat command interface for remote control
  • Scheduling system (play different content at different times)
  • Volume control integration
  • Playlist shuffle feature
  • User favorites system
  • Analytics tracking (usage statistics)
  • Particle effects synchronized with playback
  • Integration with venue lighting system
  • Touch-screen style visual interface
  • Multi-language support

Presentation Requirements

  • Professional build quality and appearance
  • Documentation notecard explaining features
  • Credit all content sources properly
  • User manual for operators
  • Demo showcasing all features
Project Tips:
  • Start with the basic structure and add features incrementally
  • Test each component thoroughly before integration
  • Keep your code organized with comments
  • Don't overcomplicate—focus on polish over quantity
  • Get feedback from others during development

Congratulations!

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.

What's Next?

  • Complete the final project
  • Explore advanced streaming platforms
  • Create your own content for broadcasting
  • Share your knowledge with the community
// --- 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 } } }

Practical Example: A Multi-Station Radio

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:

  1. Create a main box prim for the radio body.
  2. Create two smaller prims for the "Next" and "Previous" buttons.
  3. Place the buttons on the main body.
  4. Select the two buttons first, then select the main body last (the main body should have a yellow outline). Link them using Ctrl+L. The main body is now the "root" prim.
  5. Place the following script into 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);
    }
}

4. LESSON 3: Advanced Broadcasting: TV Stations, Security, and User Experience

Advanced Application: Creating a TV Station

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:

Example: Scripting a Simple Movie Screen

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);
    }
}

Real-World Scenario: Security and Access Control

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.

Example: Owner-Only Control

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.");
    }
}

Example: Group-Only Control

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.");
    }
}

Best Practices for a Great User Experience (UX)

5. HANDS-ON EXERCISES

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.

  1. Exercise 1: Set Your Parcel Mood Music.
    Find a royalty-free internet radio stream online (a good search is "free public domain radio stream"). Go to your personal parcel, open World > Parcel Details > Sound, and set the stream. Invite