The PMMS replacement the ox_core community actually deserves.
Stream YouTube videos and direct media on any TV, monitor, laptop, cinema screen, radio, speaker, or boombox in GTA V — full server-authoritative multiplayer sync, native rde_props integration, 3D proximity audio. Zero database. Zero SQL. Zero Tebex.
Built on ox_core · ox_lib · ox_target · rde_props
Built by Red Dragon Elite | SerpentsByte · v1.0.0-alpha
⚠️ EARLY ALPHA — ACTIVE DEVELOPMENTCore functionality is fully working. YouTube + direct media playback, full StateBag sync, rde_props integration and 3D audio are all production-ready. API surface may change between alpha releases.
- Why RDE OxMedia?
- Features
- Dependencies
- Installation
- Configuration
- Architecture
- rde_props Integration
- Usage
- Admin Commands
- How Multiplayer Sync Works
- Adding Custom Devices
- Adding a Language
- Exports & Developer API
- Troubleshooting
- Migrating from PMMS
- Changelog
- License
Most media scripts are either abandoned, bloated, or locked behind Tebex. RDE OxMedia is different:
| Feature | PMMS / Generic Scripts | RDE OxMedia |
|---|---|---|
| Multiplayer sync method | Legacy network events | ✅ Entity StateBags |
| rde_props integration | ❌ | ✅ GlobalState path |
| 3D proximity audio | ❌ Flat volume | ✅ Distance + room attenuation |
| Late-join sync | ❌ | ✅ Corrected playback position |
| World prop support | ❌ | ✅ Local mode, clearly labelled |
| Per-device lock | ❌ | ✅ Admin lock system |
| Permission system | ❌ | ✅ ACE + ox_core + Steam ID |
| Separate DUI resource | ✅ Required | ❌ Not needed |
| Database required | ✅ | ❌ Pure StateBag / GlobalState |
| Nostr logging | ❌ | ✅ Optional rde_nostr_log |
| Multi-language | ❌ | ✅ EN + DE built-in |
| Price | €20–€40 on Tebex | ✅ Free forever |
- StateBag multiplayer sync — play/pause/stop/volume is server-authoritative, broadcast to all clients in proximity instantly
- YouTube + direct MP4/MP3/WebM/HLS support
- 3D proximity audio — distance-based volume attenuation, room attenuation across interiors, per-device audio range
- Late-join sync — players joining mid-stream get the correct playback position automatically
- Dual interaction modes —
ox_targetthird-eye or E-key + TextUI (configurable) - Per-device admin lock — admins can lock any device so only admins can change it
- World prop support — static map TVs work in local mode, clearly labelled
- rde_props native integration — client-spawned TV/radio props sync via
GlobalState— no NetId required - DUI rendering on 40+ GTA prop models — flat-screen TVs, CRT TVs, monitors, laptops, cinema screens, arena displays, DLC props
- Three-tier entity key system —
net:<netId>·ent:<handle>·prop:<propId> - Named render targets + AddReplaceTexture fallback — full prop model coverage
- Zero database — no SQL, no table imports, pure StateBag/GlobalState architecture
- Multi-language — English + German built-in, trivially extensible
- Triple-layer permission system — ACE permissions → ox_core groups → Steam ID whitelist
- Optional rde_nostr_log integration — decentralized audit trail
- Admin
clearAll— wipe all active devices server-wide with one command
| Resource | Required | Notes |
|---|---|---|
| ox_lib | ✅ Required | UI, callbacks, commands |
| ox_core | ✅ Required | Player/character framework |
| ox_target | Only if Config.UseTarget = true |
|
| rde_props | Only if you want prop media sync | |
| rde_nostr_log | Decentralized logging — recommended |
oxmysqlis not required — this resource has zero database footprint.
cd resources
git clone https://github.com/RedDragonElite/rde_oxmedia.git
ensure ox_lib
ensure ox_core
ensure ox_target # optional — only if Config.UseTarget = true
ensure rde_props # optional — only if you want prop media sync
ensure rde_nostr_log # optional
ensure rde_oxmedia
Order matters.
rde_oxmediamust start after all its dependencies.
start rde_oxmedia
No SQL. No imports. Done.
Edit config.lua — everything is documented inline.
YouTube's IFrame API blocks nui:// origins (Error 163). The DUI must be served over HTTPS.
-- Use RDE's hosted copy — works out of the box:
Config.DuiUrl = 'https://rd-elite.com/Files/oxmedia-dui/'
-- Or self-host web/index.html anywhere over HTTPS:
-- Config.DuiUrl = 'https://your-domain.com/oxmedia-dui/'Config.Locale = GetConvar('ox:locale', 'en') -- 'en' or 'de'
-- Or in server.cfg: set ox:locale "de"Config.UseTarget = true -- true = ox_target | false = E-key + TextUIConfig.UsePermissions = true
Config.AdminSystem = {
checkOrder = { 'ace', 'oxcore', 'steam' },
acePermission = 'rde_oxmedia.admin',
oxGroups = { admin = true, superadmin = true },
steamIds = {}, -- 'steam:110000XXXXXXXXX'
}# server.cfg
add_ace group.admin rde_oxmedia.admin allow
Config.EnableAttenuation = true -- distance-based volume fade
Config.EnableRoomAttenuation = true -- quieter through walls / different rooms
Config.DiffRoomVolume = 0.3 -- volume multiplier for different rooms (0–1)Config.MaxRenderDistance = 50.0 -- DUI culled beyond this (saves VRAM)
Config.InteractionDistance = 2.5 -- E-key / TextUI / ox_target rangerde_oxmedia routes three distinct entity types through a single unified menu. Players never see the difference.
┌─────────────────────────────────────────────────────────────────────┐
│ ENTITY CLASS ROUTING │
├─────────────────┬───────────────────────┬───────────────────────────┤
│ NETWORKED │ NON-NETWORKED │ RDE_PROPS │
│ net:<netId> │ ent:<handle> │ prop:<propId> │
├─────────────────┼───────────────────────┼───────────────────────────┤
│ Player-placed │ Static world map │ Props spawned via │
│ props, vehicle │ props — world TVs, │ rde_props — client- │
│ props w/ NetId │ map decorations │ spawned, no NetId │
├─────────────────┼───────────────────────┼───────────────────────────┤
│ Entity StateBag │ Local only — each │ GlobalState[ │
│ 'oxmedia' │ client independent │ 'oxmedia_prop_<propId>'] │
├─────────────────┼───────────────────────┼───────────────────────────┤
│ All players │ You only │ All players │
│ hear the same │ │ hear the same │
└─────────────────┴───────────────────────┴───────────────────────────┘
rde_oxmedia natively supports props placed via rde_props. Because rde_props entities are client-spawned and have no NetId, a dedicated GlobalState sync path handles them — fully transparent to the player.
Player opens menu on a placed TV prop
↓
entityKey() → exports.rde_props:getPropIdByEntity(entity)
↓
propId found → key = 'prop:<propId>'
↓
TriggerServerEvent('rde_oxmedia:server:propStart', propId, data)
↓
Server validates URL + permissions
↓
GlobalState['oxmedia_prop_<propId>'] = data
↓
AddStateBagChangeHandler('', 'global') fires on ALL clients
↓
Clients with this prop entity loaded → startDevice(entity, data)
↓
DUI renders + 3D audio plays for everyone in range
rde_props v1.0.1+ — needs these exports (already present):
exports('getPropIdByEntity', function(entity) ... end)
exports('getAllEntities', function() return State.entities end)If rde_props is running when rde_oxmedia starts, everything is automatic. The menu shows a 🐉 RDE Prop badge for prop entities. No config changes needed.
- Walk up to any TV, radio, monitor, speaker in the world
- Third-eye it (ox_target) or press E (TextUI mode)
- Select
▶️ Play Media - Drop a URL:
- YouTube:
https://www.youtube.com/watch?v=VIDEO_ID - Direct:
https://example.com/video.mp4·.webm·.mp3·.m3u8
- YouTube:
- Set volume (0–100)
- Everyone nearby sees and hears the same content
| Button | Action |
|---|---|
| ⏸️ Pause / Resume | Toggle playback — synced to all |
| ⏹️ Stop | Stop media — synced to all |
| 🔊 Volume | Change volume — synced to all |
| 🔗 Change URL | Swap to a different source |
| ℹ️ Now Playing | Shows current URL |
| Command | Who | Description |
|---|---|---|
/oxmedia |
Everyone | Open media player for nearest device |
/oxmedia_stop |
Everyone | Stop media on nearest device |
/oxmedia_clear |
group.admin |
Stop all active devices server-wide |
TriggerServerEvent → validate → Entity(ent).state:set('oxmedia', data, true)
→ AddStateBagChangeHandler('oxmedia') fires on ALL clients → startDevice()
TriggerServerEvent → validate → GlobalState['oxmedia_prop_<propId>'] = data
→ AddStateBagChangeHandler('', 'global') fires on ALL clients → startDevice()
startDevice() locally — no server round-trip
→ only your client plays — labelled "🌐 Local playback only" in menu
Proximity thread runs every 2 seconds. On entering render distance of an active networked device, a server callback returns the current URL, volume, pause state, and corrected currentTime. Drift is max ~5s — negligible for streams, self-correcting for VOD.
In config.lua, add to Config.Devices:
-- TV — named render target (preferred):
{ model = 'your_prop_model', type = 'tv', audioRange = 30.0, renderTarget = 'tvscreen' },
-- TV — texture replacement fallback (no render target):
{ model = 'your_prop_model', type = 'tv', audioRange = 20.0,
screenTxd = 'your_prop_model', screenTex = 'your_prop_model_emissive_d' },
-- Radio / speaker (audio only — no screen):
{ model = 'your_speaker_model', type = 'radio', audioRange = 25.0 },Config.DeviceLookup is built automatically on resource start — no other changes needed.
- Copy
locales/en.lua→locales/xx.lua - Translate all values
Config.Locale = 'xx'orset ox:locale "xx"inserver.cfg
-- Start media on an entity locally
exports.rde_oxmedia:startDevice(entity, { url = 'https://...', volume = 80 })
-- Stop media on an entity
exports.rde_oxmedia:stopDevice(entity)
-- Get all currently active devices
local devices = exports.rde_oxmedia:getActiveDevices()-- Networked prop state
exports.rde_oxmedia:setDeviceState(netId, data)
exports.rde_oxmedia:getDeviceState(netId)
-- rde_props entity state
exports.rde_oxmedia:setPropDeviceState(propId, data)
exports.rde_oxmedia:getPropDeviceState(propId)
-- Get all active devices (both tables)
local nets = exports.rde_oxmedia:getActiveDevices()
local props = exports.rde_oxmedia:getActivePropDevices()
-- Permission check
local ok = exports.rde_oxmedia:hasPermission(source, 'admin')Black screen / no video on TV
→ Config.DuiUrl must point to a live HTTPS URL. Open it in a browser first. If it 404s the DUI page is missing.
No audio
→ Player must be within the device's audioRange. At close range EnableAttenuation still reduces volume proportionally — this is correct.
ox_target not appearing
→ ox_target must ensure before rde_oxmedia in server.cfg. Set Config.UseTarget = true.
World TV only plays for me
→ Working as intended. Static map props are non-networked GTA entities. The menu says "🌐 Local playback only". Use rde_props to spawn the prop for shared playback.
rde_props TV not syncing
→ Requires rde_props v1.0.1+. Check GetResourceState('rde_props') in F8 — must be 'started'. The 5s sync thread catches props that spawned before media started.
Late joiner has wrong playback position
→ The time reporter runs every 5s — max drift is ~5s on VOD. For live streams this is irrelevant.
1. Comment out: ensure pmms
ensure pmms-dui
2. Add: ensure rde_oxmedia
3. Set Config.DuiUrl and restart.
No data migration. StateBags are session-only. Done in 60 seconds.
Advantages over PMMS:
- StateBag/GlobalState sync vs legacy event flooding → faster, more reliable
- rde_props integration out of the box, no hacks
- No separate DUI resource needed
- Native ox_core — zero ESX shims
- Late-join sync with corrected position
- Per-device lock + full permission system
Idle CPU 5.58% → 0.89% (~84% reduction) measured in-server resmon.
- ⚡ PERF
client.lua— render threadWait(0)is now conditional: runs per-frame only whileactiveTvCount > 0(a new live counter), collapses toWait(250)in idle. This was the single largest idle cost — the coroutine was being resumed 60+ times/sec with no media playing whatsoever. - ⚡ PERF
client.lua— removed theUseTargetbackground poll thread: aGetGamePool('CObject')walk every 1000ms purely to serve the rarefindNearest/stopNearestcommands. ox_target already handles its own proximity detection viaaddModel— this was 100% duplicate work. Both commands now scan on-demand in the event handler. - ⚡ PERF
client.lua—updateAudio()(100ms tick): early-exit ifactiveDevicesis empty — skips all native calls (PlayerPedId,GetEntityCoords,GetRoomKeyFromEntity) when nothing is playing. - ⚡ PERF
client.lua— late-join sync thread interval: 2000ms → 5000ms. State catch-up on join is not gameplay-critical; the 2s cadence was unnecessary. - ⚡ PERF
client.lua—rde_propsentity lookup in the late-join sync: was callingexports['rde_props']:getPropIdByEntity()(a pcall + export hop) per pool object, per scan. Now callsgetAllEntities()once per scan and builds a reverse lookup table — N calls → 1 call. - 🔧
client.lua—activeTvCountcounter maintained onstartDevice/stopByKey; guarded withmath.max(0, ...)to prevent underflow. - 📄
README.md— troubleshooting: sync thread interval corrected from 2s → 5s.
- ✨
web/index.html— DUI page added to repo (self-hosting reference + GitHub Pages ready) - 🔧
web/—mediaelement-and-player.min.js+mediaelementplayer.min.jsremoved (unused, saves 171KB) - 🔧
fxmanifest.lua—files {}cleaned up accordingly - 🐛
web/index.html— Twitchparent=localhost→window.location.hostname(Twitch embed now works on any HTTPS host, not only localhost)
- 🔴 HOTFIX
client.lua— frozen image on stop:ReleaseNamedRendertargetdecrements ref-count but GPU retains last drawn frame; fix spawns a 2-frame thread that draws black to the render target before releasing — screen now clears correctly on stop
- 🔴 HOTFIX
fxmanifest.lua—locales/*.luare-added toshared_scripts;lib.load()usesLoadResourceFilewhich requires files registered in the manifest — causedfile 'locales.en' not foundcrash on resource start
- 🔴 BUG-01
client.lua— time reporter:dev.data.startTime→dev.startTime(wrong table level — reporter never fired since release) - 🟡 BUG-02
client.lua—ReleaseNamedRendertarget()missing instopByKey(VRAM leak on stop/start cycles) - 🔴 BUG-03
server.lua—/oxmedia_clearusedTriggerEvent→source=0, permission check bypassed,notifywas a no-op; logic inlined directly into command handler - 🟡 BUG-04
server.lua—volume=0rejected bynot volumecheck (0is truthy in Lua, usevolume == nil); players couldn't mute devices - 📄 LICENSE — Black Flag Source License v6.66 added as standalone file
- ✨ Full StateBag sync for networked props (Entity StateBag
oxmedia) - ✨ rde_props GlobalState sync path —
net:·ent:·prop:key system - ✨ Server events:
start·stop·togglePause·setVolume·lock - ✨ Prop events:
propStart·propStop·propTogglePause·propSetVolume - ✨ Admin
clearAll— wipes both networked and prop devices - ✨ Late-join proximity sync (2s) with server-corrected playback position
- ✨ Time reporter (5s) for accurate position on VOD
- ✨ 3D proximity audio — distance + room attenuation
- ✨ DUI rendering — named render targets +
AddReplaceTexturefallback - ✨ 40+ supported GTA prop models out of the box
- ✨ ox_target + E-key/TextUI dual interaction mode
- ✨ Per-device admin lock
- ✨ Triple-layer permission system (ACE / ox_core groups / Steam ID)
- ✨ Optional rde_nostr_log integration
- ✨ EN + DE locales
- 🐛 FIX: GlobalState tombstone on prop deletion — clients no longer miss stop events (Anti-Pattern #4)
- 🐛 FIX:
Log()helper — consistent[RDE_OXMEDIA]debug output, no raw prints - 🔧
immediateflag onsetPropDeviceStatefor clean resource-stop sequence
###################################################################################
# #
# .:: RED DRAGON ELITE (RDE) - BLACK FLAG SOURCE LICENSE v6.66 ::. #
# #
# PROJECT: RDE_OXMEDIA v1.0.5-ALPHA (NEXT-GEN MEDIA STREAMING FOR FIVEM) #
# ARCHITECT: .:: RDE ⧌ Shin [△ ᛋᛅᚱᛒᛅᚾᛏᛋ ᛒᛁᛏᛅ ▽] ::. | https://rd-elite.com #
# ORIGIN: https://github.com/RedDragonElite #
# #
# WARNING: THIS CODE IS PROTECTED BY DIGITAL VOODOO AND PURE HATRED FOR LEAKERS #
# #
# [ THE RULES OF THE GAME ] #
# #
# 1. // THE "FUCK GREED" PROTOCOL (FREE USE) #
# You are free to use, edit, and abuse this code on your server. #
# Learn from it. Break it. Fix it. That is the hacker way. #
# Cost: 0.00€. If you paid for this, you got scammed by a rat. #
# #
# 2. // THE TEBEX KILL SWITCH (COMMERCIAL SUICIDE) #
# Listen closely, you parasites: #
# If I find this script on Tebex, Patreon, or in a paid "Premium Pack": #
# > I will DMCA your store into oblivion. #
# > I will publicly shame your community. #
# > I hope your server lag spikes to 9999ms every time you blink. #
# SELLING FREE WORK IS THEFT. AND I AM THE JUDGE. #
# #
# 3. // THE CREDIT OATH #
# Keep this header. If you remove my name, you admit you have no skill. #
# You can add "Edited by [YourName]", but never erase the original creator. #
# Don't be a skid. Respect the architecture. #
# #
# 4. // THE CURSE OF THE COPY-PASTE #
# This code uses StateBags, GlobalState sync, and DUI rendering. #
# If you just copy-paste without reading, it WILL break. #
# Don't come crying to my DMs. RTFM or learn to code. #
# #
# -------------------------------------------------------------------------- #
# "We build the future on the graves of paid resources." #
# "REJECT MODERN MEDIOCRITY. EMBRACE RDE SUPERIORITY." #
# -------------------------------------------------------------------------- #
###################################################################################
TL;DR:
- ✅ Free forever — use it, edit it, learn from it
- ✅ Keep the header — credit where it's due
- ❌ Don't sell it — commercial use = instant DMCA
- ❌ Don't be a skid — copy-paste without reading won't work anyway
| 🐙 GitHub | RedDragonElite |
| 🌍 Website | rd-elite.com |
| 🔵 Nostr (RDE) | RedDragonElite |
| 🔵 Nostr (Shin) | SerpentsByte |
| 📺 RDE OxMedia | rde_oxmedia |
| 🎯 RDE Props | rde_props |
| 🚪 RDE Doors | rde_doors |
| 😴 RDE Sleep | rde_sleep |
| 📡 RDE Nostr Log | rde_nostr_log |
When asking for help, always include:
- Full error from server console or txAdmin
- Your
server.cfgresource start order - ox_core / ox_lib versions in use
Please DON'T:
- ❌ DM for basic setup questions — read the docs first
- ❌ Open issues without error logs
- ❌ Ask for paid support — this is free software
Please DO:
- ✅ Star the repo if it helped you
- ✅ Open issues with proper reproduction steps
- ✅ Share your setup — community feedback makes this better
"We build the future on the graves of paid resources."
REJECT MODERN MEDIOCRITY. EMBRACE RDE SUPERIORITY.
🐉 Made with 🔥 by Red Dragon Elite