Why Custom Radio Matters in Roleplay
Music and radio are essential atmospheric elements that bring a FiveM roleplay server to life. While GTA V ships with its own radio stations, custom radio systems let server owners create themed stations that match their community identity, stream live DJ sets, play community-curated playlists, and even broadcast in-character news bulletins. A well-built music system goes beyond simple audio playback. It provides synchronized listening experiences for players in the same vehicle, spatial audio from boomboxes and club speakers, personal playlists that players can manage through a phone app, and DJ booth integration for nightclub roleplay. The technical foundation relies on NUI audio capabilities, which let you leverage the full power of browser-based audio APIs within the FiveM client.
Audio Architecture and NUI Integration
FiveM's NUI layer runs a Chromium-based browser embedded in the game client, which means you have access to the Web Audio API, HTML5 audio elements, and even the MediaSource API for adaptive streaming. The architecture splits into three components: the Lua client script handles game-world interactions like entering vehicles or approaching speakers, the NUI HTML and JavaScript layer manages actual audio playback and visualization, and the server coordinates synchronized playback states across multiple clients. When a player enters a vehicle, the client script sends an NUI message to start playing the current station. The NUI layer creates an audio element, sets the source URL, and begins playback. Volume controls are handled entirely in the NUI layer using the Web Audio API's GainNode for smooth volume transitions:
-- Client-side: Vehicle radio controller
local currentStation = nil
local radioActive = false
CreateThread(function()
while true do
Wait(500)
local ped = PlayerPedId()
if IsPedInAnyVehicle(ped, false) then
local vehicle = GetVehiclePedIsIn(ped, false)
if not radioActive then
radioActive = true
-- Disable default GTA radio
SetVehicleRadioEnabled(vehicle, false)
-- Load saved station preference
local savedStation = LocalPlayer.state.radioStation or 'station_1'
SetStation(savedStation)
end
elseif radioActive then
radioActive = false
SendNUIMessage({action = 'stopRadio'})
currentStation = nil
end
end
end)
function SetStation(stationId)
local station = Config.Stations[stationId]
if not station then return end
currentStation = stationId
SendNUIMessage({
action = 'playStation',
url = station.url,
name = station.name,
volume = LocalPlayer.state.radioVolume or 0.5,
})
end
NUI Audio Player Setup
The NUI side needs a robust audio player that handles stream connections, buffering, error recovery, and volume management. Use the Web Audio API rather than simple HTML5 audio elements because it gives you control over audio routing, allows you to create visualizations from frequency data, and supports spatial audio processing. Create an AudioContext with a GainNode for volume control and an AnalyserNode for visualization data:
// NUI JavaScript: Audio engine
class RadioPlayer {
constructor() {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.gainNode = this.audioContext.createGain();
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 256;
this.gainNode.connect(this.analyser);
this.analyser.connect(this.audioContext.destination);
this.audio = new Audio();
this.audio.crossOrigin = 'anonymous';
this.source = null;
this.isPlaying = false;
}
play(url, volume) {
this.stop();
this.audio = new Audio(url);
this.audio.crossOrigin = 'anonymous';
this.audio.volume = 1.0;
this.source = this.audioContext.createMediaElementSource(this.audio);
this.source.connect(this.gainNode);
this.gainNode.gain.value = volume;
this.audio.play().then(() => {
this.isPlaying = true;
}).catch(err => {
console.error('Radio playback failed:', err);
setTimeout(() => this.play(url, volume), 3000);
});
}
stop() {
if (this.audio) {
this.audio.pause();
this.audio.src = '';
this.isPlaying = false;
}
}
setVolume(vol) {
this.gainNode.gain.linearRampToValueAtTime(
vol, this.audioContext.currentTime + 0.1
);
}
getFrequencyData() {
const data = new Uint8Array(this.analyser.frequencyBinCount);
this.analyser.getByteFrequencyData(data);
return data;
}
}
const radio = new RadioPlayer();
Station Configuration and Streaming
Define your radio stations with stream URLs, metadata, and genre categories. Most custom FiveM radio systems use internet radio streams in MP3 or AAC format because they are widely supported and easy to set up. You can host your own Icecast or Shoutcast server for complete control, or use public radio stream URLs. For each station, store the stream URL, a display name, a genre tag, and optionally a logo image for the UI. Consider adding support for YouTube and SoundCloud URLs through proxy services, though be mindful of terms of service and copyright restrictions. For live DJ functionality, integrate with services that provide RTMP-to-HTTP stream conversion so DJs can broadcast using OBS or similar software:
Config.Stations = {
['station_1'] = {
name = 'Agency Radio',
genre = 'Hip Hop',
url = 'https://stream.example.com/hiphop',
logo = 'station_hiphop.webp',
description = 'The freshest beats in Los Santos',
},
['station_2'] = {
name = 'LS Rock Radio',
genre = 'Rock',
url = 'https://stream.example.com/rock',
logo = 'station_rock.webp',
description = 'Classic and modern rock anthems',
},
['station_3'] = {
name = 'Vinewood Lounge',
genre = 'Jazz / Lofi',
url = 'https://stream.example.com/lofi',
logo = 'station_lofi.webp',
description = 'Smooth vibes for the night drive',
},
['station_live'] = {
name = 'Live DJ',
genre = 'Live',
url = 'https://stream.example.com/live',
logo = 'station_live.webp',
description = 'Live DJ sets from the community',
isLive = true,
},
}
Synchronized Vehicle Radio
When multiple players ride in the same vehicle, they should all hear the same radio station. This requires server-side coordination through state bags or networked events. When the driver changes the station, the server broadcasts the new station to all occupants. Use FiveM's state bag system for efficient synchronization because it automatically handles network replication without manual event management. Set the station on the vehicle entity's state bag so any player entering the vehicle automatically picks up the current station. Handle edge cases like passengers entering a vehicle that already has a station playing, the driver leaving while passengers remain, and conflicting station changes from passengers if you allow non-driver control:
-- Server-side: Sync radio state via state bags
RegisterNetEvent('radio:server:setStation', function(vehicleNet, stationId)
local src = source
local vehicle = NetworkGetEntityFromNetworkId(vehicleNet)
if not DoesEntityExist(vehicle) then return end
-- Verify player is the driver
local ped = GetPlayerPed(src)
if GetPedInVehicleSeat(vehicle, -1) ~= ped then return end
-- Set station on vehicle state bag
Entity(vehicle).state:set('radioStation', stationId, true)
end)
-- Client-side: Listen for state bag changes
AddStateBagChangeHandler('radioStation', nil, function(bagName, key, value)
if not value then return end
local entity = GetEntityFromStateBagName(bagName)
if not DoesEntityExist(entity) then return end
local ped = PlayerPedId()
if not IsPedInVehicle(ped, entity, false) then return end
SetStation(value)
end)
Spatial Audio and Boombox Systems
Spatial audio adds a layer of immersion that flat audio playback cannot match. Boomboxes, club speakers, and car stereos should emit sound that fades with distance and pans based on the listener's position relative to the source. FiveM provides native audio functions like PlaySoundFromCoord for simple spatial sounds, but for streaming audio you need to implement distance-based volume scaling in the NUI layer. Calculate the distance between the player and the audio source every frame, then map that distance to a volume curve. Use an inverse-square falloff for realistic sound attenuation, with configurable minimum and maximum hearing distances. For boomboxes that players can place in the world, create a deployable item that spawns a prop at the player's location and registers it as an audio source. Other players within range should automatically hear the music, with volume scaling based on their distance from the boombox prop.
Player Playlist and Phone Integration
Give players personal control over their music experience through a playlist system integrated with your server's phone resource. Players should be able to create custom playlists by adding URLs to individual tracks, reorder songs with drag-and-drop, set a playlist to shuffle or repeat, and share playlists with other players. Store playlists in the database linked to the player's identifier so they persist across sessions. The phone app interface should display the currently playing track with artist and title metadata, playback controls for play, pause, skip, and previous, a volume slider, and a list of saved playlists. When the player is in a vehicle, the phone player should automatically route audio through the vehicle's radio system so passengers can hear the music. When on foot with earbuds equipped as an item, the music plays privately with a small earphone icon visible to other players, indicating that the character is listening to music without it being audible to others.
Visualizer and UI Design
A music player UI should feel alive and responsive to the audio being played. Use the AnalyserNode from the Web Audio API to extract frequency data in real time and render an audio visualizer. Bar visualizers, waveform displays, and circular spectrum analyzers all work well depending on your UI aesthetic. Render the visualizer using a canvas element for performance, updating at 30 or 60 frames per second depending on the complexity of the visualization. The radio UI itself should be minimal and non-intrusive, appearing as a small widget in the corner of the screen showing the current station name, track title if available from stream metadata, and basic playback controls. When the player opens the full radio menu to switch stations, display a grid or list of available stations with their logos, genres, and listener counts. Include a search bar for servers with many stations and a favorites system so players can quickly access their preferred stations without scrolling through the entire list.
Performance Considerations
Audio streaming consumes bandwidth and client resources, so optimize your implementation carefully. Limit the number of simultaneous audio streams per client to prevent memory leaks and CPU spikes. When a player is not in a vehicle and has no boombox nearby, completely destroy the audio context rather than just pausing it. For spatial audio from multiple boomboxes, only create audio streams for sources within hearing range and clean up streams as sources move out of range. Use low-bitrate streams for ambient background music and reserve higher quality streams for the primary listening experience. On the server side, avoid sending stream URLs directly in events because they can be intercepted and abused. Instead, use a callback system where the client requests the stream URL for a specific station ID and the server responds with a time-limited signed URL. Monitor client-reported playback errors and automatically reconnect streams that drop due to network issues, with exponential backoff to prevent hammering a struggling stream server.