├── package.json ├── html ├── assets │ ├── materialdesignicons-webfont.eot │ ├── materialdesignicons-webfont.ttf │ ├── materialdesignicons-webfont.woff │ ├── materialdesignicons-webfont.woff2 │ └── webfontloader.js ├── index.html └── vite.svg ├── [src] ├── src │ ├── plugins │ │ ├── vuetify.js │ │ └── webfontloader.js │ ├── main.js │ ├── assets │ │ ├── vue.svg │ │ └── logo.svg │ ├── style.css │ ├── components │ │ ├── controllers │ │ │ ├── SoundName.vue │ │ │ ├── Timeline.vue │ │ │ └── ControlButtons.vue │ │ ├── Radio.vue │ │ ├── Equalizer.vue │ │ ├── AddSong.vue │ │ ├── Slider.vue │ │ └── Playlist.vue │ ├── App.vue │ └── store │ │ └── index.js ├── index.html ├── README.md ├── vite.config.js ├── package.json └── public │ └── vite.svg ├── fxmanifest.lua ├── config.lua ├── LICENSE ├── server ├── sv_api.js ├── classes │ └── radio.lua └── sv_main.lua ├── README.md ├── yarn.lock └── client └── cl_main.lua /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "progress-estimator": "^0.3.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /html/assets/materialdesignicons-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xBlackAttack/x99-vehicleradio/HEAD/html/assets/materialdesignicons-webfont.eot -------------------------------------------------------------------------------- /html/assets/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xBlackAttack/x99-vehicleradio/HEAD/html/assets/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /html/assets/materialdesignicons-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xBlackAttack/x99-vehicleradio/HEAD/html/assets/materialdesignicons-webfont.woff -------------------------------------------------------------------------------- /html/assets/materialdesignicons-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xBlackAttack/x99-vehicleradio/HEAD/html/assets/materialdesignicons-webfont.woff2 -------------------------------------------------------------------------------- /[src]/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | // Styles 2 | import '@mdi/font/css/materialdesignicons.css' 3 | import 'vuetify/styles' 4 | 5 | // Vuetify 6 | import { createVuetify } from 'vuetify' 7 | 8 | export default createVuetify( 9 | // https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides 10 | ) 11 | -------------------------------------------------------------------------------- /[src]/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import "./style.css" 3 | import App from './App.vue' 4 | import store from './store' 5 | import vuetify from './plugins/vuetify' 6 | import { loadFonts } from './plugins/webfontloader' 7 | 8 | loadFonts() 9 | 10 | createApp(App) 11 | .use(store) 12 | .use(vuetify) 13 | .mount('#app') 14 | -------------------------------------------------------------------------------- /[src]/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vuetify 3 Vite Preview 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /[src]/src/plugins/webfontloader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/webfontloader.js 3 | * 4 | * webfontloader documentation: https://github.com/typekit/webfontloader 5 | */ 6 | 7 | export async function loadFonts () { 8 | const webFontLoader = await import(/* webpackChunkName: "webfontloader" */'webfontloader') 9 | 10 | webFontLoader.load({ 11 | google: { 12 | families: ['Roboto:100,300,400,500,700,900&display=swap'], 13 | }, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /[src]/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vuetify 3 Vite Preview 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /[src]/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + Vite 2 | 3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` 23 | 24 | -------------------------------------------------------------------------------- /[src]/src/components/Radio.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 | -------------------------------------------------------------------------------- /[src]/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fqx-vehicleradio", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vite preview", 7 | "build": "vite build --base=./ --emptyOutDir", 8 | "dev": "vite --host", 9 | "preview": "vite preview", 10 | "watch": "vite build --base=./ --emptyOutDir --watch" 11 | }, 12 | "dependencies": { 13 | "@mdi/font": "5.9.55", 14 | "axios": "^1.4.0", 15 | "roboto-fontface": "*", 16 | "vue": "^3.2.47", 17 | "vuetify": "^3.0.0-beta.0", 18 | "vuex": "^4.0.0", 19 | "webfontloader": "^1.0.0" 20 | }, 21 | "devDependencies": { 22 | "@vitejs/plugin-vue": "^4.1.0", 23 | "@vue/cli-plugin-vuex": "~5.0.0", 24 | "sass": "^1.63.6", 25 | "sass-loader": "^13.3.2", 26 | "vite": "^4.3.9", 27 | "vite-plugin-vuetify": "^1.0.0-alpha.12", 28 | "vue-cli-plugin-vuetify": "~2.5.8" 29 | }, 30 | "type": "module" 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 BlackAttack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /html/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /[src]/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/sv_api.js: -------------------------------------------------------------------------------- 1 | const youtubedl = require('youtube-dl-exec') 2 | const logger = require('progress-estimator')() 3 | 4 | const cachedYT = {} 5 | 6 | const loadYT = async (id) => { 7 | if (cachedYT[id]) return cachedYT[id]; 8 | 9 | const videoURL = `https://www.youtube.com/watch?v=${id}`; 10 | const promise = youtubedl(videoURL, { 11 | dumpSingleJson: true, 12 | noCheckCertificates: true, 13 | // noWarnings: true, 14 | preferFreeFormats: true, 15 | addHeader: [ 16 | 'referer:youtube.com', 17 | 'user-agent:googlebot' 18 | ] 19 | 20 | }) 21 | 22 | // promise.then((result) => { 23 | // console.log(result) 24 | // }).catch((err) => { 25 | // console.log(err) 26 | // }) 27 | 28 | const result = await logger(promise, `Obtaining ${id}`) 29 | 30 | const title = result.title 31 | const duration = result.duration 32 | const audioFormat = result.requested_formats.find((x) => x.resolution == "audio only") 33 | const audioURL = audioFormat.url 34 | 35 | return cachedYT[id] = { 36 | title: title, 37 | duration: duration, 38 | url: audioURL, 39 | } 40 | } 41 | 42 | // onNet('bs-vehicleradio:api:loadYT', async (id) => { 43 | // const src = source; 44 | // const yt = await loadYT(id) 45 | // emitNet('bs-vehicleradio:api:client:loadYT', src, yt) 46 | // }) 47 | 48 | globalThis.exports('loadYT', async (id) => { 49 | const yt = await loadYT(id) 50 | return yt 51 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A lot of people were asking because we removed it from Tebex. You can use it as open source. 2 | 3 | There is no support for this script. Items that need to be added are available in the file. 4 | 5 | This script was last updated in 2024. There may be security issues or it may not work. You are solely responsible. 6 | 7 | There will be no updates from my side, but I will check the PRs. 8 | 9 | | Export Function | Description | 10 | | --------------------------------------------------------------------------- | ---------------------------------------------------- | 11 | | `x99-vehicleradio:client:openRadio` | This function shows vehicleradio menu | 12 | 13 | 14 | ## First of all, you need to install youtube-dl-exec in your package, for this you need to open CMD to the location where FXServer.exe is located and type `npm i youtube-dl-exec` and install it. Otherwise the script will not work. 15 | 16 | 17 | 18 | https://user-images.githubusercontent.com/42780579/268454383-05874e3c-e479-4029-888a-ec864082cfd2.mp4 19 | 20 | 21 | 22 | 23 | If you want to do it using an item, follow this. 24 | 25 | 26 | ```lua 27 | QBCore.Functions.CreateUseableItem('vehicleradio', function(source) 28 | TriggerClientEvent("x99-vehicleradio:client:openRadio", source) 29 | end) 30 | ``` 31 | 32 | ^ add this to server side. 33 | 34 | 35 | ```lua 36 | { 37 | id = 'openradio', 38 | title = 'Open Radio', 39 | icon = 'car-side', 40 | type = 'client', 41 | event = 'x99-vehicleradio:client:openRadio', 42 | shouldClose = true 43 | } 44 | ``` 45 | 46 | ^ For Radial Menu 47 | 48 | 49 | If you are using ESX and want to add item for Vehicle Radio 50 | 51 | ```lua 52 | ESX.RegisterUsableItem('vehicleradio', function(source) 53 | TriggerClientEvent("x99-vehicleradio:client:openRadio", source) 54 | end) 55 | ``` 56 | -------------------------------------------------------------------------------- /server/classes/radio.lua: -------------------------------------------------------------------------------- 1 | VehicleRadio = {} 2 | 3 | function GenerateRandomString(length) 4 | local str = "" 5 | for i = 1, length do 6 | str = str .. string.char(math.random(97, 122)) 7 | end 8 | return str 9 | end 10 | 11 | function VehicleRadio:create(netId, source) 12 | local this = {} 13 | local vehicle = NetworkGetEntityFromNetworkId(netId) 14 | 15 | this.id = netId--GenerateRandomString(8) 16 | this.vehicle = vehicle 17 | this.source = source 18 | this.currentSong = "" 19 | this.playlist = {} 20 | this.volume = 15 21 | this.bassBoost = 0 22 | this.trebleBoost = 0 23 | this.currentTime = 0 24 | this.isPlaying = false 25 | 26 | self.__index = self 27 | return setmetatable(this, self) 28 | end 29 | 30 | function VehicleRadio:addSong(data, id) 31 | for k, v in pairs(self.playlist) do 32 | if v == id then return end 33 | end 34 | 35 | table.insert(self.playlist, { 36 | id = id, 37 | title = data.title, 38 | url = data.url, 39 | }) 40 | 41 | if #self.playlist == 1 then 42 | self:playSong(id) 43 | self.isPlaying = true 44 | end 45 | end 46 | 47 | function VehicleRadio:playSong(id) 48 | self.currentSong = id 49 | self.currentTime = 0 50 | self.isPlaying = true 51 | end 52 | 53 | function VehicleRadio:syncVolume(volume) 54 | self.volume = volume 55 | end 56 | 57 | function VehicleRadio:syncBassBoost(bassBoost) 58 | self.bassBoost = bassBoost 59 | end 60 | 61 | function VehicleRadio:syncTrebleBoost(trebleBoost) 62 | self.trebleBoost = trebleBoost 63 | end 64 | 65 | function VehicleRadio:syncSeekTo(time) 66 | self.currentTime = time 67 | end 68 | 69 | function VehicleRadio:syncPause(isPlaying) 70 | self.isPlaying = isPlaying 71 | end 72 | 73 | function VehicleRadio:syncCurrentTime(time) 74 | self.currentTime = time 75 | end -------------------------------------------------------------------------------- /[src]/src/components/Equalizer.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 62 | 63 | -------------------------------------------------------------------------------- /[src]/src/components/controllers/Timeline.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 79 | 80 | -------------------------------------------------------------------------------- /[src]/src/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 66 | 67 | -------------------------------------------------------------------------------- /[src]/src/components/AddSong.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 68 | 69 | -------------------------------------------------------------------------------- /[src]/src/components/Slider.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 80 | 81 | -------------------------------------------------------------------------------- /[src]/src/components/Playlist.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 92 | 93 | -------------------------------------------------------------------------------- /[src]/src/components/controllers/ControlButtons.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 108 | 109 | -------------------------------------------------------------------------------- /server/sv_main.lua: -------------------------------------------------------------------------------- 1 | RegisteredRadios = {} 2 | 3 | CreateThread(function() 4 | GlobalState.RegisteredRadios = {} 5 | 6 | while true do 7 | for netId, radio in pairs(RegisteredRadios) do 8 | local vehicle = NetworkGetEntityFromNetworkId(netId) 9 | if not DoesEntityExist(vehicle) or GetVehicleNumberPlateText(vehicle) ~= radio.plate then 10 | RegisteredRadios[netId] = nil 11 | GlobalState.RegisteredRadios = RegisteredRadios 12 | TriggerClientEvent("x99-vehicleradio:sync:unregisterRadio", -1, netId) 13 | end 14 | end 15 | Wait(1000 * 2) 16 | end 17 | end) 18 | 19 | RegisterNetEvent("x99-vehicleradio:registerRadio", function(netId) 20 | local source = source 21 | if not RegisteredRadios[netId] then 22 | RegisteredRadios[netId] = VehicleRadio:create(netId, source) 23 | TriggerClientEvent("x99-vehicleradio:sync:registerRadio", -1, netId) 24 | end 25 | local vehicle = NetworkGetEntityFromNetworkId(netId) 26 | local radio = RegisteredRadios[netId] 27 | radio.plate = GetVehicleNumberPlateText(vehicle) 28 | GlobalState.RegisteredRadios = RegisteredRadios 29 | return radio.id 30 | end) 31 | 32 | RegisterNetEvent("x99-vehicleradio:addSong", function(data, netId) 33 | local source = source 34 | local id = data.id 35 | local radio = RegisteredRadios[netId] 36 | if not radio then 37 | return false 38 | end 39 | 40 | local ytData = exports[GetCurrentResourceName()]:loadYT(id) 41 | if not ytData then 42 | return false 43 | end 44 | 45 | radio:addSong(ytData, id) 46 | radio.source = source 47 | TriggerClientEvent("x99-vehicleradio:sync:addSong", -1, netId, id, ytData.title, ytData.url) 48 | GlobalState.RegisteredRadios = RegisteredRadios 49 | return true 50 | end) 51 | 52 | RegisterNetEvent("x99-vehicleradio:syncVolume", function(vol, netId) 53 | local source = source 54 | local radio = RegisteredRadios[netId] 55 | if not radio then 56 | return 57 | end 58 | 59 | radio.source = source 60 | radio:syncVolume(vol) 61 | TriggerClientEvent("x99-vehicleradio:sync:syncVolume", -1, netId, vol) 62 | GlobalState.RegisteredRadios = RegisteredRadios 63 | return radio.id 64 | end) 65 | 66 | RegisterNetEvent("x99-vehicleradio:syncBassBoost", function(boost, netId) 67 | local source = source 68 | local radio = RegisteredRadios[netId] 69 | if not radio then 70 | return 71 | end 72 | radio.source = source 73 | radio:syncBassBoost(boost) 74 | TriggerClientEvent("x99-vehicleradio:sync:syncBassBoost", -1, netId, boost) 75 | -- print("syncBassBoost", netId, boost) 76 | GlobalState.RegisteredRadios = RegisteredRadios 77 | return radio.id 78 | end) 79 | 80 | RegisterNetEvent("x99-vehicleradio:syncTrebleBoost", function(boost, netId) 81 | local source = source 82 | local radio = RegisteredRadios[netId] 83 | if not radio then 84 | return 85 | end 86 | radio.source = source 87 | radio:syncTrebleBoost(boost) 88 | TriggerClientEvent("x99-vehicleradio:sync:syncTrebleBoost", -1, netId, boost) 89 | -- print("syncTrebleBoost", netId, boost) 90 | GlobalState.RegisteredRadios = RegisteredRadios 91 | return radio.id 92 | end) 93 | 94 | RegisterNetEvent("x99-vehicleradio:syncSeekTo", function(seek, netId) 95 | local source = source 96 | local radio = RegisteredRadios[netId] 97 | if not radio then 98 | return 99 | end 100 | radio.source = source 101 | radio:syncSeekTo(seek) 102 | TriggerClientEvent("x99-vehicleradio:sync:syncSeekTo", -1, netId, seek) 103 | GlobalState.RegisteredRadios = RegisteredRadios 104 | return radio.id 105 | end) 106 | 107 | RegisterNetEvent("x99-vehicleradio:syncPause", function(pause, netId, currentTime) 108 | local source = source 109 | local radio = RegisteredRadios[netId] 110 | if not radio then 111 | return 112 | end 113 | radio.source = source 114 | radio:syncPause(pause) 115 | radio:syncSeekTo(currentTime) 116 | TriggerClientEvent("x99-vehicleradio:sync:syncPause", -1, netId, pause, currentTime) 117 | GlobalState.RegisteredRadios = RegisteredRadios 118 | return radio.id 119 | end) 120 | 121 | RegisterNetEvent("x99-vehicleradio:playSong", function(id, netId) 122 | local source = source 123 | local radio = RegisteredRadios[netId] 124 | if not radio then 125 | return 126 | end 127 | radio.source = source 128 | radio:playSong(id) 129 | 130 | TriggerClientEvent("x99-vehicleradio:sync:playSong", -1, netId, id) 131 | GlobalState.RegisteredRadios = RegisteredRadios 132 | return radio.id 133 | end) 134 | 135 | RegisterNetEvent("x99-vehicleradio:syncCurrentTime", function(time, netId) 136 | local source = source 137 | local radio = RegisteredRadios[netId] 138 | if not radio then 139 | return 140 | end 141 | radio.source = source 142 | radio:syncCurrentTime(time) 143 | GlobalState.RegisteredRadios = RegisteredRadios 144 | return radio.id 145 | end) 146 | 147 | AddStateBagChangeHandler("radioTime", nil, function(bagName, value, data) 148 | local netId = bagName:gsub("entity:", "") 149 | if not netId then return end 150 | netId = tonumber(netId) 151 | local radio = RegisteredRadios[netId] 152 | if not radio then return end 153 | 154 | radio:syncCurrentTime(data) 155 | GlobalState.RegisteredRadios = RegisteredRadios 156 | end) -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | ansi-escapes@^3.0.0: 6 | version "3.2.0" 7 | resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz" 8 | integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== 9 | 10 | ansi-regex@^3.0.0: 11 | version "3.0.1" 12 | resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz" 13 | integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== 14 | 15 | ansi-styles@^3.2.1: 16 | version "3.2.1" 17 | resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" 18 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 19 | dependencies: 20 | color-convert "^1.9.0" 21 | 22 | chalk@^2.4.1: 23 | version "2.4.2" 24 | resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" 25 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 26 | dependencies: 27 | ansi-styles "^3.2.1" 28 | escape-string-regexp "^1.0.5" 29 | supports-color "^5.3.0" 30 | 31 | cli-cursor@^2.0.0: 32 | version "2.1.0" 33 | resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz" 34 | integrity sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw== 35 | dependencies: 36 | restore-cursor "^2.0.0" 37 | 38 | cli-spinners@^1.3.1: 39 | version "1.3.1" 40 | resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz" 41 | integrity sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg== 42 | 43 | color-convert@^1.9.0: 44 | version "1.9.3" 45 | resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" 46 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 47 | dependencies: 48 | color-name "1.1.3" 49 | 50 | color-name@1.1.3: 51 | version "1.1.3" 52 | resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" 53 | integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== 54 | 55 | escape-string-regexp@^1.0.5: 56 | version "1.0.5" 57 | resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" 58 | integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== 59 | 60 | has-flag@^3.0.0: 61 | version "3.0.0" 62 | resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" 63 | integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== 64 | 65 | humanize-duration@^3.15.3: 66 | version "3.28.0" 67 | resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.28.0.tgz" 68 | integrity sha512-jMAxraOOmHuPbffLVDKkEKi/NeG8dMqP8lGRd6Tbf7JgAeG33jjgPWDbXXU7ypCI0o+oNKJFgbSB9FKVdWNI2A== 69 | 70 | is-fullwidth-code-point@^2.0.0: 71 | version "2.0.0" 72 | resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz" 73 | integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== 74 | 75 | log-update@^2.3.0: 76 | version "2.3.0" 77 | resolved "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz" 78 | integrity sha512-vlP11XfFGyeNQlmEn9tJ66rEW1coA/79m5z6BCkudjbAGE83uhAcGYrBFwfs3AdLiLzGRusRPAbSPK9xZteCmg== 79 | dependencies: 80 | ansi-escapes "^3.0.0" 81 | cli-cursor "^2.0.0" 82 | wrap-ansi "^3.0.1" 83 | 84 | mimic-fn@^1.0.0: 85 | version "1.2.0" 86 | resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz" 87 | integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== 88 | 89 | onetime@^2.0.0: 90 | version "2.0.1" 91 | resolved "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz" 92 | integrity sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ== 93 | dependencies: 94 | mimic-fn "^1.0.0" 95 | 96 | progress-estimator@^0.3.1: 97 | version "0.3.1" 98 | resolved "https://registry.npmjs.org/progress-estimator/-/progress-estimator-0.3.1.tgz" 99 | integrity sha512-I5bwE35adOrA2rfZ9iHHPESUp5C6cXrZd0J8LdFD9J+66ijSugBwZHooPCROf94Ox8YZaUyvTLViqSiRvBhSoA== 100 | dependencies: 101 | chalk "^2.4.1" 102 | cli-spinners "^1.3.1" 103 | humanize-duration "^3.15.3" 104 | log-update "^2.3.0" 105 | 106 | restore-cursor@^2.0.0: 107 | version "2.0.0" 108 | resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz" 109 | integrity sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q== 110 | dependencies: 111 | onetime "^2.0.0" 112 | signal-exit "^3.0.2" 113 | 114 | signal-exit@^3.0.2: 115 | version "3.0.7" 116 | resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" 117 | integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== 118 | 119 | string-width@^2.1.1: 120 | version "2.1.1" 121 | resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz" 122 | integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== 123 | dependencies: 124 | is-fullwidth-code-point "^2.0.0" 125 | strip-ansi "^4.0.0" 126 | 127 | strip-ansi@^4.0.0: 128 | version "4.0.0" 129 | resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz" 130 | integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== 131 | dependencies: 132 | ansi-regex "^3.0.0" 133 | 134 | supports-color@^5.3.0: 135 | version "5.5.0" 136 | resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" 137 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 138 | dependencies: 139 | has-flag "^3.0.0" 140 | 141 | wrap-ansi@^3.0.1: 142 | version "3.0.1" 143 | resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz" 144 | integrity sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ== 145 | dependencies: 146 | string-width "^2.1.1" 147 | strip-ansi "^4.0.0" 148 | -------------------------------------------------------------------------------- /html/assets/webfontloader.js: -------------------------------------------------------------------------------- 1 | function Ft(p,S){for(var x=0;xh[m]})}}}return Object.freeze(Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}))}function Pt(p){return p&&p.__esModule&&Object.prototype.hasOwnProperty.call(p,"default")?p.default:p}var at={exports:{}};(function(p){(function(){function S(t,n,e){return t.call.apply(t.bind,arguments)}function x(t,n,e){if(!t)throw Error();if(2=n.f?a():t.fonts.load(ht(n.a),n.h).then(function(c){1<=c.length?r():setTimeout(f,25)},function(){a()})}f()}),o=null,s=new Promise(function(r,a){o=setTimeout(a,n.f)});Promise.race([s,i]).then(function(){o&&(clearTimeout(o),o=null),n.g(n.a)},function(){n.j(n.a)})};function V(t,n,e,i,o,s,r){this.v=t,this.B=n,this.c=e,this.a=i,this.s=r||"BESbswy",this.f={},this.w=o||3e3,this.u=s||null,this.m=this.j=this.h=this.g=null,this.g=new b(this.c,this.s),this.h=new b(this.c,this.s),this.j=new b(this.c,this.s),this.m=new b(this.c,this.s),t=new v(this.a.c+",serif",g(this.a)),t=C(t),this.g.a.style.cssText=t,t=new v(this.a.c+",sans-serif",g(this.a)),t=C(t),this.h.a.style.cssText=t,t=new v("serif",g(this.a)),t=C(t),this.j.a.style.cssText=t,t=new v("sans-serif",g(this.a)),t=C(t),this.m.a.style.cssText=t,A(this.g),A(this.h),A(this.j),A(this.m)}var O={D:"serif",C:"sans-serif"},W=null;function X(){if(W===null){var t=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent);W=!!t&&(536>parseInt(t[1],10)||parseInt(t[1],10)===536&&11>=parseInt(t[2],10))}return W}V.prototype.start=function(){this.f.serif=this.j.a.offsetWidth,this.f["sans-serif"]=this.m.a.offsetWidth,this.A=m(),Q(this)};function J(t,n,e){for(var i in O)if(O.hasOwnProperty(i)&&n===t.f[O[i]]&&e===t.f[O[i]])return!0;return!1}function Q(t){var n=t.g.a.offsetWidth,e=t.h.a.offsetWidth,i;(i=n===t.f.serif&&e===t.f["sans-serif"])||(i=X()&&J(t,n,e)),i?m()-t.A>=t.w?X()&&J(t,n,e)&&(t.u===null||t.u.hasOwnProperty(t.a.c))?I(t,t.v):I(t,t.B):mt(t):I(t,t.v)}function mt(t){setTimeout(h(function(){Q(this)},t),50)}function I(t,n){setTimeout(h(function(){E(this.g.a),E(this.h.a),E(this.j.a),E(this.m.a),n(this.a)},t),0)}function k(t,n,e){this.c=t,this.a=n,this.f=0,this.m=this.j=!1,this.s=e}var N=null;k.prototype.g=function(t){var n=this.a;n.g&&y(n.f,[n.a.c("wf",t.c,g(t).toString(),"active")],[n.a.c("wf",t.c,g(t).toString(),"loading"),n.a.c("wf",t.c,g(t).toString(),"inactive")]),j(n,"fontactive",t),this.m=!0,Y(this)},k.prototype.h=function(t){var n=this.a;if(n.g){var e=D(n.f,n.a.c("wf",t.c,g(t).toString(),"active")),i=[],o=[n.a.c("wf",t.c,g(t).toString(),"loading")];e||i.push(n.a.c("wf",t.c,g(t).toString(),"inactive")),y(n.f,i,o)}j(n,"fontinactive",t),Y(this)};function Y(t){--t.f==0&&t.j&&(t.m?(t=t.a,t.g&&y(t.f,[t.a.c("wf","active")],[t.a.c("wf","loading"),t.a.c("wf","inactive")]),j(t,"active")):R(t.a))}function Z(t){this.j=t,this.a=new vt,this.h=0,this.f=this.g=!0}Z.prototype.load=function(t){this.c=new T(this.j,t.context||this.j),this.g=t.events!==!1,this.f=t.classes!==!1,yt(this,new pt(this.c,t),t)};function wt(t,n,e,i,o){var s=--t.h==0;(t.f||t.g)&&setTimeout(function(){var r=o||null,a=i||null||{};if(e.length===0&&s)R(n.a);else{n.f+=e.length,s&&(n.j=s);var f,c=[];for(f=0;f 1 then 48 | if IsVehicleDoorDamaged(vehicle, i) ~= 1 then 49 | local door = GetVehicleDoorAngleRatio(vehicle, i + 1) 50 | local winId = doorIdsToWindows[i] 51 | local window = IsVehicleWindowIntact(vehicle, winId) 52 | 53 | totalDoorAngle += door > 0 and door or window and 0 or 0.9 54 | else 55 | totalDoorAngle += 1 56 | brokenDoors += 1 57 | end 58 | doorCount += 1 59 | end 60 | end 61 | return totalDoorAngle, doorCount, brokenDoors 62 | end 63 | 64 | CreateThread(function() 65 | activeTexts = {} 66 | local function DrawScreenText(x, y, text, justify) 67 | SetTextFont(0) 68 | SetTextProportional(1) 69 | SetTextScale(0.0, 0.23) 70 | SetTextColour(255, 255, 255, 255) 71 | SetTextDropshadow(0, 0, 0, 0, 255) 72 | SetTextEdge(2, 0, 0, 0, 150) 73 | SetTextDropShadow() 74 | SetTextOutline() 75 | SetTextJustification(justify or 1) 76 | SetTextEntry("STRING") 77 | AddTextComponentString(text) 78 | DrawText(x, y) 79 | end 80 | 81 | while true do 82 | local msec = 1000 83 | if DEBUG then 84 | msec = 1 85 | for i = 1, #activeTexts do 86 | local text = activeTexts[i] 87 | if text then 88 | local yOffset = #activeTexts - i 89 | local t_len = string.len(text) 90 | DrawScreenText(0.005, 0.68 - yOffset * 0.025, text, 2) 91 | end 92 | end 93 | end 94 | Wait(msec) 95 | end 96 | end) 97 | 98 | tprint = function(...) 99 | local args = {...} 100 | for i = 1, #args do 101 | if type(args[i]) == "table" then 102 | args[i] = "\n" .. json.encode(args[i], {indent = true}) 103 | end 104 | end 105 | _print(table.unpack(args)) 106 | end 107 | 108 | RegisterNetEvent("x99-vehicleradio:client:openRadio", function() 109 | if not isInVeh then return end 110 | registerRadio() 111 | displayRadio() 112 | end) 113 | 114 | function registerRadio() 115 | local vehicle = GetVehiclePedIsIn(PlayerPedId(), false) 116 | if vehicle == 0 then return end 117 | 118 | local netId = NetworkGetNetworkIdFromEntity(vehicle) 119 | TriggerServerEvent("x99-vehicleradio:registerRadio", netId) 120 | playerRadioId = netId 121 | SendNUIMessage({ 122 | type = "register", 123 | radio = playerRadioId, 124 | }) 125 | Wait(200) 126 | end 127 | 128 | function displayRadio(bool) 129 | if bool == nil then 130 | bool = not radioDisplay 131 | end 132 | radioDisplay = bool 133 | SetNuiFocus(radioDisplay, radioDisplay) 134 | SendNUIMessage({ 135 | type = "display", 136 | display = radioDisplay 137 | }) 138 | end 139 | 140 | -- function getJSObject(gstate) 141 | -- local radios = gstate or GlobalState.RegisteredRadios 142 | -- local _table = {} 143 | -- for netId, radio in pairs(radios) do 144 | -- _table[tostring(netId)] = radio 145 | -- end 146 | -- return _table 147 | -- end 148 | 149 | RegisterNUICallback("addSong", function(data, cb) 150 | local vehicle = GetVehiclePedIsIn(PlayerPedId(), false) 151 | -- if vehicle == 0 then return end 152 | 153 | local netId = NetworkGetNetworkIdFromEntity(vehicle) 154 | TriggerServerEvent("x99-vehicleradio:addSong", data, netId) 155 | cb(1) 156 | end) 157 | 158 | RegisterNUICallback("syncVolume", function(data, cb) 159 | local vehicle = GetVehiclePedIsIn(PlayerPedId(), false) 160 | if vehicle == 0 then return end 161 | 162 | local netId = NetworkGetNetworkIdFromEntity(vehicle) 163 | TriggerServerEvent("x99-vehicleradio:syncVolume", data.volume, netId) 164 | cb(1) 165 | end) 166 | 167 | RegisterNUICallback("syncBassBoost", function(data, cb) 168 | local vehicle = GetVehiclePedIsIn(PlayerPedId(), false) 169 | if vehicle == 0 then return end 170 | 171 | local netId = NetworkGetNetworkIdFromEntity(vehicle) 172 | TriggerServerEvent("x99-vehicleradio:syncBassBoost", data.bassBoost, netId) 173 | cb(1) 174 | end) 175 | 176 | RegisterNUICallback("syncTrebleBoost", function(data, cb) 177 | local vehicle = GetVehiclePedIsIn(PlayerPedId(), false) 178 | if vehicle == 0 then return end 179 | 180 | local netId = NetworkGetNetworkIdFromEntity(vehicle) 181 | TriggerServerEvent("x99-vehicleradio:syncTrebleBoost", data.trebleBoost, netId) 182 | cb(1) 183 | end) 184 | 185 | RegisterNUICallback("syncSeekTo", function(data, cb) 186 | local vehicle = GetVehiclePedIsIn(PlayerPedId(), false) 187 | if vehicle == 0 then return end 188 | 189 | local netId = NetworkGetNetworkIdFromEntity(vehicle) 190 | TriggerServerEvent("x99-vehicleradio:syncSeekTo", data.time, netId) 191 | cb(1) 192 | end) 193 | 194 | RegisterNUICallback("togglePause", function(data, cb) 195 | local vehicle = GetVehiclePedIsIn(PlayerPedId(), false) 196 | if vehicle == 0 then return end 197 | local netId = data.radioId or NetworkGetNetworkIdFromEntity(vehicle) 198 | TriggerServerEvent("x99-vehicleradio:syncPause", data.isPlaying, netId, clientTime) 199 | cb(1) 200 | end) 201 | 202 | RegisterNUICallback("playSong", function(data, cb) 203 | local vehicle = GetVehiclePedIsIn(PlayerPedId(), false) 204 | if vehicle == 0 then return end 205 | 206 | local netId = NetworkGetNetworkIdFromEntity(vehicle) 207 | TriggerServerEvent("x99-vehicleradio:playSong", data.songId, netId) 208 | cb(1) 209 | end) 210 | 211 | RegisterNUICallback("display", function(data, cb) 212 | displayRadio(data.display) 213 | cb(1) 214 | end) 215 | 216 | AddStateBagChangeHandler("RegisteredRadios", "global", function(bagName, _, data) 217 | for key, queue in pairs(radiosSyncQueue) do 218 | for k, v in pairs(queue) do 219 | local id = v.netId 220 | local radio = data[id] 221 | if radio then 222 | SendNUIMessage({ 223 | type = "syncGlobal", 224 | key = key, 225 | radio = radio, 226 | args = v 227 | }) 228 | table.remove(queue, k) 229 | end 230 | end 231 | end 232 | end) 233 | 234 | RegisterNetEvent("x99-vehicleradio:sync:registerRadio", function(netId) 235 | table.insert(radiosSyncQueue["registerRadio"], { 236 | netId = netId, 237 | }) 238 | end) 239 | 240 | RegisterNetEvent("x99-vehicleradio:sync:unregisterRadio", function(netId) 241 | SendNUIMessage({ 242 | type = "unregister", 243 | radio = netId, 244 | }) 245 | end) 246 | 247 | RegisterNetEvent("x99-vehicleradio:sync:addSong", function(netId, songId, title, url) 248 | table.insert(radiosSyncQueue["addSong"], { 249 | netId = netId, 250 | songId = songId, 251 | title = title, 252 | url = url 253 | }) 254 | end) 255 | 256 | RegisterNetEvent("x99-vehicleradio:sync:syncVolume", function(netId, vol) 257 | table.insert(radiosSyncQueue["syncVolume"], { 258 | netId = netId, 259 | volume = vol 260 | }) 261 | end) 262 | 263 | RegisterNetEvent("x99-vehicleradio:sync:syncBassBoost", function(netId, bassBoost) 264 | table.insert(radiosSyncQueue["syncBassBoost"], { 265 | netId = netId, 266 | bassBoost = bassBoost 267 | }) 268 | end) 269 | 270 | RegisterNetEvent("x99-vehicleradio:sync:syncTrebleBoost", function(netId, trebleBoost) 271 | table.insert(radiosSyncQueue["syncTrebleBoost"], { 272 | netId = netId, 273 | trebleBoost = trebleBoost 274 | }) 275 | end) 276 | 277 | RegisterNetEvent("x99-vehicleradio:sync:syncSeekTo", function(netId, time) 278 | table.insert(radiosSyncQueue["syncSeekTo"], { 279 | netId = netId, 280 | time = time 281 | }) 282 | end) 283 | 284 | RegisterNetEvent("x99-vehicleradio:sync:playSong", function(netId, songId) 285 | table.insert(radiosSyncQueue["playSong"], { 286 | netId = netId, 287 | songId = songId 288 | }) 289 | end) 290 | 291 | RegisterNetEvent("x99-vehicleradio:sync:syncPause", function(netId, isPlaying, time) 292 | -- table.insert(radiosSyncQueue["syncPause"], { 293 | -- netId = netId, 294 | -- isPlaying = isPlaying 295 | -- }) 296 | 297 | SendNUIMessage({ 298 | type = "syncGlobal", 299 | key = "syncPause", 300 | args = { 301 | netId = netId, 302 | isPlaying = isPlaying, 303 | time = time 304 | } 305 | }) 306 | end) 307 | 308 | CreateThread(function() 309 | while true do 310 | local inVeh = IsPedInAnyVehicle(PlayerPedId(), false) 311 | 312 | if inVeh and not isInVeh then 313 | isInVeh = true 314 | -- playerRadioId = NetworkGetNetworkIdFromEntity(GetVehiclePedIsIn(PlayerPedId(), false)) 315 | elseif not inVeh and isInVeh then 316 | isInVeh = false 317 | playerRadioId = nil 318 | displayRadio(false) 319 | end 320 | 321 | Wait(250) 322 | end 323 | end) 324 | 325 | local deg = math.deg 326 | local acos = math.acos 327 | local pi = math.pi 328 | local atan2 = math.atan2 329 | 330 | 331 | CreateThread(function() 332 | local vehiclesDoesNotExist = {} 333 | while true do 334 | if cachedGlobalState then 335 | local playerPed = PlayerPedId() 336 | local pos = GetPedBoneCoords(playerPed, 0x796e) 337 | local heading = GetGameplayCamRot(0) 338 | local direction = RotationToDirection(heading) 339 | 340 | for netId, radio in pairs(cachedGlobalState) do 341 | local vehicle = NetworkDoesNetworkIdExist(netId) and NetworkGetEntityFromNetworkId(netId) or 0 342 | if not muteRadio and DoesEntityExist(vehicle) then 343 | local isInRadioVeh = GetVehiclePedIsIn(playerPed, false) == vehicle 344 | local vehPos = GetEntityCoords(vehicle) 345 | local doorAngle, totalDoors = GetAverageDoorAngles(vehicle) 346 | local totalAngleRatio = doorAngle / totalDoors 347 | 348 | local dist = #(pos - vehPos) 349 | local volumeDist = (radio.volume * 0.8)-- * (totalAngleRatio * 4) 350 | 351 | if not isInRadioVeh and dist < volumeDist then -- and radio.isPlaying 352 | local volume = math.max(0.0, 1.0 - (dist / volumeDist)) 353 | 354 | local playerToVehicleDirection = (vehPos - pos) 355 | local crossProductZ = -playerToVehicleDirection.x * direction.y + playerToVehicleDirection.y * direction.x 356 | local stereoBalance = crossProductZ 357 | local stereoSpread = 0.5 358 | 359 | local leftBalance = (1.0 - stereoSpread) * (0.5 + 0.5 * stereoBalance) 360 | local rightBalance = (1.0 - stereoSpread) * (0.5 - 0.5 * stereoBalance) 361 | 362 | leftBalance = math.max(0.0, math.min(5.0, leftBalance)) -- (volume / 2) 363 | rightBalance = math.max(0.0, math.min(5.0, rightBalance)) -- (volume / 2) 364 | 365 | if not Config.useStereo then 366 | leftBalance = 5.0 367 | rightBalance = 5.0 368 | end 369 | 370 | if volume > 0 then 371 | SendNUIMessage({ 372 | type = "updatePosition", 373 | netId = netId, 374 | leftBalance = leftBalance, 375 | rightBalance = rightBalance, 376 | volume = volume, 377 | LPFValue = Config.useLPF and (2.5 * (1 - totalAngleRatio)) * dist, 378 | }) 379 | end 380 | elseif not isInRadioVeh then 381 | SendNUIMessage({ 382 | type = "updatePosition", 383 | netId = netId, 384 | leftBalance = 0.0, 385 | rightBalance = 0.0, 386 | volume = 0.0 387 | }) 388 | elseif isInRadioVeh then 389 | SendNUIMessage({ 390 | type = "updatePosition", 391 | netId = netId, 392 | leftBalance = 5.0, 393 | rightBalance = 5.0, 394 | volume = 1.0 395 | }) 396 | end 397 | if vehiclesDoesNotExist[netId] then 398 | vehiclesDoesNotExist[netId] = nil 399 | end 400 | elseif not vehiclesDoesNotExist[netId] then 401 | vehiclesDoesNotExist[netId] = true 402 | SendNUIMessage({ 403 | type = "updatePosition", 404 | netId = netId, 405 | leftBalance = 0.0, 406 | rightBalance = 0.0, 407 | volume = 0.0 408 | }) 409 | end 410 | end 411 | end 412 | Wait(80) 413 | end 414 | end) 415 | 416 | CreateThread(function() 417 | local _before = {} 418 | while true do 419 | if cachedGlobalState and _before then 420 | for netId, radio in pairs(_before) do 421 | local vehicle = NetworkDoesNetworkIdExist(netId) and NetworkGetEntityFromNetworkId(netId) or 0 422 | local vehicleExist = DoesEntityExist(vehicle) 423 | local dist = #(GetEntityCoords(vehicle) - GetEntityCoords(PlayerPedId())) 424 | -- print(dist) 425 | if vehicleExist and not vehiclesInPlayerScope[netId] and dist < 200 then 426 | -- print("vehicleEnteredScope", netId) 427 | vehiclesInPlayerScope[netId] = true 428 | SendNUIMessage({ 429 | type = "vehicleEnteredScope", 430 | netId = netId, 431 | data = cachedGlobalState[netId] 432 | }) 433 | elseif not vehicleExist and vehiclesInPlayerScope[netId] or (dist > 200 and vehiclesInPlayerScope[netId]) then 434 | -- print("vehicleLeftScope", netId) 435 | vehiclesInPlayerScope[netId] = nil 436 | SendNUIMessage({ 437 | type = "vehicleLeftScope", 438 | netId = netId, 439 | }) 440 | elseif _before[netId] and not cachedGlobalState[netId] then 441 | vehiclesInPlayerScope[netId] = nil 442 | SendNUIMessage({ 443 | type = "vehicleLeftScope", 444 | netId = netId 445 | }) 446 | end 447 | end 448 | end 449 | _before = cachedGlobalState 450 | Wait(1000) 451 | end 452 | end) 453 | 454 | AddStateBagChangeHandler("RegisteredRadios", "global", function(bagName, value, data) 455 | cachedGlobalState = data 456 | activeTexts = {} 457 | for k, v in pairs(cachedGlobalState) do 458 | for k1, v1 in pairs(v) do 459 | activeTexts[#activeTexts + 1] = "\"".. k1 .. "\": "..json.encode(v1) 460 | end 461 | end 462 | end) 463 | 464 | function RotationToDirection(rotation) 465 | local adjustedRotation = 466 | { 467 | x = (math.pi / 180) * rotation.x, 468 | y = (math.pi / 180) * rotation.y, 469 | z = (math.pi / 180) * rotation.z 470 | } 471 | local direction = 472 | { 473 | x = -math.sin(adjustedRotation.z) * math.abs(math.cos(adjustedRotation.x)), 474 | y = math.cos(adjustedRotation.z) * math.abs(math.cos(adjustedRotation.x)), 475 | z = math.sin(adjustedRotation.x) 476 | } 477 | return vector3(direction.x, direction.y, direction.z) 478 | end 479 | 480 | RegisterNUICallback("requestRadioSync", function(data, cb) 481 | SendNUIMessage({ 482 | type = "config", 483 | maxVolume = Config.maxVolume, 484 | bassRange = Config.bassRange, 485 | trebleRange = Config.trebleRange, 486 | }) 487 | SendNUIMessage({ 488 | type = "debug", 489 | debug = DEBUG 490 | }) 491 | Wait(1000) -- 10000 492 | cachedGlobalState = GlobalState.RegisteredRadios 493 | local radiosObject = {} 494 | 495 | for netId, radio in pairs(cachedGlobalState) do 496 | radiosObject[tostring(netId)] = radio 497 | end 498 | 499 | -- SendNUIMessage({ 500 | -- type = "syncRadios", 501 | -- radios = radiosObject 502 | -- }) 503 | cb(1) 504 | end) 505 | 506 | RegisterNUICallback("timeupdate", function(data, cb) 507 | cb(1) 508 | local time = data.time 509 | local netId = data.radioId or playerRadioId 510 | if not vehiclesInPlayerScope[netId] then return end 511 | local radio = cachedGlobalState[netId] 512 | local playerId = GetPlayerServerId(PlayerId()) 513 | if not radio or radio.source ~= playerId then 514 | -- print("Radio not found", netId, time, playerId, radio and radio.source) 515 | return 516 | end 517 | 518 | local vehicle = NetworkGetEntityFromNetworkId(netId) 519 | if not DoesEntityExist(vehicle) then 520 | -- print("Vehicle not found", netId, time, playerId, radio and radio.source) 521 | return 522 | end 523 | 524 | Entity(vehicle).state:set("radioTime", time, true) 525 | end) 526 | 527 | if Config.muteRadioCommand then 528 | RegisterCommand(Config.muteRadioCommand, function() 529 | local text = "Radio is now " 530 | muteRadio = not muteRadio 531 | if muteRadio then 532 | text = text .. "muted" 533 | else 534 | text = text .. "unmuted" 535 | end 536 | 537 | TriggerEvent("chat:addMessage", { 538 | color = {255, 0, 0}, 539 | multiline = true, 540 | args = {"Radio", text} 541 | }) 542 | end) 543 | end -------------------------------------------------------------------------------- /[src]/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | import axios from "axios"; 3 | let DEBUG = false 4 | 5 | let resourceName = "x99-vehicleradio" 6 | 7 | if (window.GetParentResourceName) { 8 | resourceName = GetParentResourceName() 9 | } 10 | 11 | const post = (url, data) => { 12 | try { 13 | return fetch(`http://${resourceName}/${url}`, { 14 | method: "POST", 15 | body: JSON.stringify(data), 16 | headers: { 17 | "Content-Type": "application/json", 18 | }, 19 | }).then((response) => response && response.json()); 20 | } catch (error) { 21 | // debuglog("post error", error); 22 | } 23 | }; 24 | 25 | const debuglog = (...args) => { 26 | if (DEBUG) { 27 | console.log(...args) 28 | } 29 | } 30 | 31 | const validQueryDomains = new Set([ 32 | 'youtube.com', 33 | 'www.youtube.com', 34 | 'm.youtube.com', 35 | 'music.youtube.com', 36 | 'gaming.youtube.com', 37 | ]); 38 | const urlRegex = /^https?:\/\//; 39 | const getVideoID = str => { 40 | if (validateID(str)) { 41 | return str; 42 | } else if (urlRegex.test(str.trim())) { 43 | return getURLVideoID(str); 44 | } else { 45 | throw Error(`No video id found: ${str}`); 46 | } 47 | }; 48 | 49 | const idRegex = /^[a-zA-Z0-9-_]{11}$/; 50 | const validateID = id => idRegex.test(id.trim()); 51 | 52 | 53 | const validPathDomains = /^https?:\/\/(youtu\.be\/|(www\.)?youtube\.com\/(embed|v|shorts)\/)/; 54 | const getURLVideoID = link => { 55 | const parsed = new URL(link.trim()); 56 | let id = parsed.searchParams.get('v'); 57 | if (validPathDomains.test(link.trim()) && !id) { 58 | const paths = parsed.pathname.split('/'); 59 | id = parsed.host === 'youtu.be' ? paths[1] : paths[2]; 60 | } else if (parsed.hostname && !validQueryDomains.has(parsed.hostname)) { 61 | throw Error('Not a YouTube domain'); 62 | } 63 | if (!id) { 64 | throw Error(`No video id found: "${link}"`); 65 | } 66 | id = id.substring(0, 11); 67 | if (!validateID(id)) { 68 | throw TypeError(`Video id (${id}) does not match expected ` + 69 | `format (${idRegex.toString()})`); 70 | } 71 | return id; 72 | }; 73 | 74 | 75 | const store = createStore({ 76 | state: { 77 | globalRadios: [], 78 | playerRadioId: null, 79 | 80 | vehiclesInScope: [], 81 | 82 | displayRadio: false, 83 | displayPlaylist: false, 84 | displayAddSong: false, 85 | displayEqualizer: false, 86 | 87 | maxVolume: 50, 88 | bassRange: { min: -10, max: 22 }, 89 | trebleRange: { min: -10, max: 22 }, 90 | progress: "", 91 | }, 92 | actions: { 93 | getRadioById({ state }, id) { 94 | return state.globalRadios.find(radio => radio.id == id) 95 | }, 96 | 97 | displayPlaylist({ state }, display) { 98 | state.displayPlaylist = display == null ? !state.displayPlaylist : display 99 | }, 100 | displayAddSong({ state }, display) { 101 | state.displayAddSong = display == null ? !state.displayAddSong : display 102 | }, 103 | displayEqualizer({ state }, display) { 104 | state.displayEqualizer = display == null ? !state.displayEqualizer : display 105 | }, 106 | 107 | async addSongToPlaylist({ state }, { url }) { 108 | const id = getVideoID(url) 109 | state.progress = "Processing..."; 110 | post("addSong", { 111 | id 112 | }) 113 | }, 114 | 115 | async createAudioSource({ state, dispatch }, { radioId, songId }) { 116 | const radio = state.globalRadios.find(radio => radio.id == radioId) 117 | if (!radio) return debuglog("[createAudioSource] Radio not found", radioId, radio); 118 | const song = radio.playlist.find(song => song.id == songId); 119 | // if (!song) return debuglog("[createAudioSource] Song not found", songId, radio); 120 | // if (!radio.isPlaying) return debuglog("[createAudioSource] Radio is not playing", radioId, radio); 121 | 122 | if (!radio.audioContext) { 123 | radio.audioContext = new AudioContext(); 124 | debuglog("[createAudioSource] AudioContext created", radio.audioContext); 125 | } 126 | 127 | if (!radio.audioElement) { 128 | radio.audioElement = document.createElement("audio", { id: `audio-${radioId}` }); 129 | radio.audioElement.crossOrigin = "anonymous"; 130 | radio.audioElement.preload = "metadata"; 131 | radio.audioElement.volume = 0; 132 | radio.audioElement.loop = false; 133 | radio.audioElement.autoplay = false; 134 | radio.audioElement.controls = false; 135 | radio.audioElement.id = `audio-${radioId}`; 136 | if (song) { 137 | radio.audioElement.src = song.url; 138 | } 139 | 140 | debuglog("[createAudioSource] AudioElement created", radio.audioElement, song, radio.audioElement.volume); 141 | } 142 | 143 | if (!radio.sourceNode) { 144 | radio.sourceNode = radio.audioContext.createMediaElementSource(radio.audioElement); 145 | debuglog("[createAudioSource] SourceNode created", radio.sourceNode); 146 | } 147 | 148 | const audio = await dispatch("createFilters", { radioId }); 149 | 150 | audio.addEventListener('timeupdate', function () { 151 | radio.currentTime = audio.currentTime 152 | post("timeupdate", { time: radio.currentTime, radioId: radioId }) 153 | }); 154 | 155 | radio.currentSong = songId 156 | audio.addEventListener('loadedmetadata', async function () { 157 | const newSong = radio.playlist.find(song => song.id == radio.currentSong); 158 | audio.volume = radio.volume / 100 159 | radio.title = newSong.title 160 | radio.duration = audio.duration 161 | 162 | dispatch("syncPause", { isPlaying: radio.isPlaying, radioId: radioId }) 163 | debuglog("[createAudioSource] Song loaded", radio, newSong, radio.currentSong); 164 | audio.currentTime = radio.currentTime 165 | }); 166 | 167 | audio.addEventListener('ended', function () { 168 | debuglog("[createAudioSource] Song ended", radio); 169 | dispatch("syncPause", { isPlaying: false, radioId: radioId }) 170 | dispatch("playNextSong", { radioId: radioId }) 171 | }); 172 | 173 | audio.addEventListener('pause', function () { 174 | radio.isPlaying = false 175 | }); 176 | 177 | audio.addEventListener('play', function () { 178 | radio.isPlaying = true 179 | }); 180 | }, 181 | 182 | updateVolume({ state }, { volume, radioId }) { 183 | radioId = radioId || state.playerRadioId 184 | const radio = state.globalRadios.find(radio => radio.id == radioId) 185 | if (!radio) return debuglog("[updateVolume] Radio not found", radioId, radio); 186 | 187 | radio.volume = volume 188 | if (radio.audioElement) { 189 | radio.audioElement.volume = (volume / 100) * radio.distancedVolume 190 | } 191 | }, 192 | updateBassBoost({ state }, { bassBoost, radioId }) { 193 | radioId = radioId || state.playerRadioId 194 | const radio = state.globalRadios.find(radio => radio.id == radioId) 195 | if (!radio) return debuglog("[updateBassBoost] Radio not found", radioId, radio); 196 | 197 | if (radio.bassFilter) { 198 | radio.bassFilter.gain.value = bassBoost 199 | } else { 200 | debuglog("[updateBassBoost] BassFilter not found", radioId, radio); 201 | } 202 | 203 | radio.bassBoost = bassBoost 204 | }, 205 | updateTrebleBoost({ state }, { trebleBoost, radioId }) { 206 | radioId = radioId || state.playerRadioId 207 | const radio = state.globalRadios.find(radio => radio.id == radioId) 208 | if (!radio) return debuglog("[updateTrebleBoost] Radio not found", radioId, radio); 209 | if (radio.trebleFilter) { 210 | radio.trebleFilter.gain.value = trebleBoost 211 | } else { 212 | debuglog("[updateTrebleBoost] TrebleFilter not found", radioId, radio); 213 | } 214 | 215 | radio.trebleBoost = trebleBoost 216 | }, 217 | updateCurrentTime({ state }, { time, radioId }) { 218 | radioId = radioId || state.playerRadioId 219 | const radio = state.globalRadios.find(radio => radio.id == radioId) 220 | if (!radio) return debuglog("[updateCurrentTime] Radio not found", radioId, radio); 221 | if (time > radio.duration) return debuglog("[updateCurrentTime] Time is bigger than duration", time, radio.duration); 222 | if (time < 0) return debuglog("[updateCurrentTime] Time is smaller than 0", time, radio.duration); 223 | if (time == Infinity) return debuglog("[updateCurrentTime] Time is Infinity", time, radio.duration); 224 | 225 | radio.currentTime = time 226 | if (radio.audioElement) { 227 | radio.audioElement.currentTime = time 228 | } 229 | }, 230 | 231 | togglePause({ state }, { play, radioId }) { 232 | radioId = radioId || state.playerRadioId 233 | const radio = state.globalRadios.find(radio => radio.id == radioId) 234 | if (!radio) return debuglog("[togglePause] Radio not found", radioId, radio); 235 | radio.isPlaying = play != null ? play : !radio.isPlaying 236 | 237 | debuglog("play", play, radio.isPlaying) 238 | // if (radio.isPlaying) { 239 | // radio.audioElement.pause() 240 | // } else { 241 | // radio.audioElement.play() 242 | // } 243 | 244 | post("togglePause", { isPlaying: radio.isPlaying }) 245 | }, 246 | 247 | playSong({ state }, { radioId, songId }) { 248 | radioId = radioId || state.playerRadioId 249 | const radio = state.globalRadios.find(radio => radio.id == radioId) 250 | if (!radio) return debuglog("[playSong] Radio not found", radioId, radio); 251 | const song = radio.playlist.find(song => song.id == songId); 252 | if (!song) return debuglog("[playSong] Song not found", song, songId, radio); 253 | state.progress = ""; 254 | radio.currentSong = songId; 255 | radio.audioElement.src = song.url; 256 | radio.audioElement.volume = 0; 257 | radio.audioElement.play(); 258 | }, 259 | 260 | syncVolume({ state }) { 261 | const radio = state.globalRadios.find(radio => radio.id == state.playerRadioId) 262 | if (!radio) return debuglog("[syncVolume] Radio not found", state.playerRadioId, radio); 263 | 264 | post("syncVolume", { volume: radio.volume }) 265 | }, 266 | syncBassBoost({ state }) { 267 | const radio = state.globalRadios.find(radio => radio.id == state.playerRadioId) 268 | if (!radio) return debuglog("[syncBassBoost] Radio not found", state.playerRadioId, radio); 269 | 270 | post("syncBassBoost", { bassBoost: radio.bassFilter.gain.value }) 271 | }, 272 | syncTrebleBoost({ state }) { 273 | const radio = state.globalRadios.find(radio => radio.id == state.playerRadioId) 274 | if (!radio) return debuglog("[syncTrebleBoost] Radio not found", state.playerRadioId, radio); 275 | 276 | post("syncTrebleBoost", { trebleBoost: radio.trebleFilter.gain.value }) 277 | }, 278 | syncSeekTo({ state }, { time, radioId }) { 279 | radioId = radioId || state.playerRadioId 280 | const radio = state.globalRadios.find(radio => radio.id == radioId) 281 | if (!radio) return debuglog("[updateCurrentTime] Radio not found", radioId, radio); 282 | 283 | if (time > radio.duration) return debuglog("[updateCurrentTime] Time is bigger than duration", time, radio.duration); 284 | if (time < 0) return debuglog("[updateCurrentTime] Time is smaller than 0", time, radio.duration); 285 | if (time == Infinity) return debuglog("[updateCurrentTime] Time is Infinity", time, radio.duration); 286 | 287 | post("syncSeekTo", { time }) 288 | }, 289 | syncPause({ state }, { radioId, isPlaying, time }) { 290 | radioId = radioId || state.playerRadioId 291 | const radio = state.globalRadios.find(radio => radio.id == radioId) 292 | if (!radio) return debuglog("[syncPause] Radio not found", radioId, radio); 293 | 294 | radio.isPlaying = isPlaying 295 | if (radio.isPlaying) { 296 | radio.audioElement.play() 297 | radio.audioElement.volume = 0; 298 | } else { 299 | radio.audioElement.pause() 300 | } 301 | 302 | if (time) { 303 | radio.audioElement.currentTime = time 304 | } 305 | }, 306 | 307 | 308 | syncPlaySong({ state, dispatch }, { radioId, songId }) { 309 | radioId = radioId || state.playerRadioId 310 | const radio = state.globalRadios.find(radio => radio.id == radioId) 311 | if (!radio) return debuglog("[syncPlaySong] Radio not found", radioId, radio); 312 | const song = radio.playlist.find(song => song.id == songId); 313 | if (!song) return debuglog("[syncPlaySong] Song not found", songId, radio); 314 | state.progress = ""; 315 | // if (radio.audioElement) { 316 | // radio.audioElement.src = "data:audio/mp3;base64," + song.base64; 317 | // radio.audioElement.play(); 318 | // } else { 319 | // dispatch("createAudioSource", { radioId, songId }) 320 | // } 321 | 322 | post("playSong", { radioId, songId }) 323 | }, 324 | 325 | 326 | buttonBackward({ state, dispatch }) { 327 | const radio = state.globalRadios.find(radio => radio.id == state.playerRadioId) 328 | if (!radio) return debuglog("[buttonBackward] Radio not found", state.playerRadioId, radio); 329 | 330 | if (radio.audioElement.currentTime > 5) { 331 | dispatch("syncSeekTo", { time: 0 }) 332 | } else { 333 | const index = radio.playlist.findIndex(song => song.id == radio.currentSong) 334 | if (index == -1) return debuglog("[buttonBackward] Song not found", radio.currentSong, radio.playlist); 335 | if (!radio.playlist[index - 1]) return debuglog("[buttonBackward] Song not found", radio.currentSong, radio.playlist); 336 | 337 | dispatch("playSong", { radioId: state.playerRadioId, songId: radio.playlist[index - 1].id }) 338 | } 339 | }, 340 | 341 | buttonForward({ state }) { 342 | const radio = state.globalRadios.find(radio => radio.id == state.playerRadioId) 343 | if (!radio) return debuglog("[buttonForward] Radio not found", state.playerRadioId, radio); 344 | 345 | const index = radio.playlist.findIndex(song => song.id == radio.currentSong) 346 | if (index == -1) return debuglog("[buttonForward] Song not found", radio.currentSong, radio.playlist); 347 | 348 | post("playSong", { radioId: state.playerRadioId, songId: radio.playlist[index + 1].id }) 349 | }, 350 | 351 | createFilters({ state }, { radioId }) { 352 | const radio = state.globalRadios.find(radio => radio.id == radioId) 353 | if (!radio) return debuglog("[createFilters] Radio not found", radioId, radio); 354 | 355 | const audio = radio.audioElement; 356 | const source = radio.sourceNode; 357 | const context = radio.audioContext; 358 | 359 | const bassFilter = context.createBiquadFilter(); 360 | bassFilter.type = "lowshelf"; 361 | bassFilter.frequency.value = 60; 362 | bassFilter.gain.value = radio.bassBoost; 363 | radio.bassFilter = bassFilter; 364 | debuglog("[createAudioSource] BassFilter created", bassFilter); 365 | 366 | const trebleFilter = context.createBiquadFilter(); 367 | trebleFilter.type = "highshelf"; 368 | trebleFilter.frequency.value = 10000; 369 | trebleFilter.gain.value = radio.trebleBoost; 370 | radio.trebleFilter = trebleFilter; 371 | debuglog("[createAudioSource] TrebleFilter created", trebleFilter); 372 | 373 | const stereoPanner = context.createStereoPanner(); 374 | stereoPanner.pan.value = radio.stereoPan; 375 | radio.stereoPanner = stereoPanner; 376 | debuglog("[createAudioSource] StereoPanner created", stereoPanner); 377 | 378 | const LPFilter = context.createBiquadFilter(); 379 | LPFilter.type = "lowpass"; 380 | LPFilter.frequency.value = 20000; 381 | radio.LPFilter = LPFilter; 382 | 383 | source.connect(bassFilter); 384 | bassFilter.connect(trebleFilter); 385 | trebleFilter.connect(stereoPanner); 386 | stereoPanner.connect(LPFilter); 387 | LPFilter.connect(context.destination); 388 | 389 | return audio; 390 | } 391 | }, 392 | getters: { 393 | playerRadio(state) { 394 | if (!state.playerRadioId) return null 395 | return state.globalRadios.find(radio => radio.id == state.playerRadioId) 396 | } 397 | } 398 | }) 399 | 400 | window.addEventListener('message', async (event) => { 401 | const data = event.data; 402 | const state = store.state; 403 | const dispatch = store.dispatch; 404 | 405 | if (data.type != "updatePosition" && data.type != "updatePositionFar") { 406 | // debuglog("data", data) 407 | } 408 | 409 | if (data.type === "display") { 410 | state.displayRadio = data.display 411 | } 412 | 413 | if (data.type === "register") { 414 | state.playerRadioId = data.radio 415 | } 416 | 417 | if (data.type === "unregister") { 418 | const radio = state.globalRadios.find(radio => radio.id == data.radio) 419 | if (!radio) return debuglog("[unregister] Radio not found", data.radio, radio); 420 | 421 | radio.audioElement.pause() 422 | radio.audioElement.src = "" 423 | radio.audioElement.volume = 0 424 | radio.audioElement = null 425 | 426 | state.globalRadios = state.globalRadios.filter(radio => radio.id != data.radio) 427 | 428 | if (state.playerRadioId == data.radio) { 429 | state.playerRadioId = null 430 | } 431 | } 432 | 433 | if (data.type === "update") { 434 | let radios = {} 435 | for (const [key, value] of Object.entries(data.radios)) { 436 | radios[key] = value 437 | } 438 | state.globalRadios = radios 439 | } 440 | 441 | if (data.type === "playGlobal") { 442 | // dispatch("createAudioSource", data) 443 | } 444 | 445 | if (data.type === "syncGlobal") { 446 | const rId = data.args.netId 447 | 448 | if (data.key === "addSong") { 449 | const id = data.args.songId 450 | const radio = await dispatch("getRadioById", rId) 451 | if (radio) { 452 | if (radio.playlist.find(song => song.id == id)) { 453 | debuglog("[addSong] song already exists", id); return; 454 | } 455 | 456 | const mp3 = { 457 | title: data.args.title, 458 | url: data.args.url, 459 | id: id, 460 | } 461 | 462 | radio.playlist.push(mp3) 463 | 464 | if (radio.playlist.length === 1) { 465 | if (state.vehiclesInScope.includes(rId)) { 466 | dispatch("playSong", { songId: mp3.id, radioId: rId }) 467 | } 468 | } 469 | 470 | 471 | 472 | store.state.progress = ""; 473 | } 474 | } 475 | 476 | if (data.key == "registerRadio") { 477 | if (await dispatch("getRadioById", rId)) { 478 | debuglog("[registerRadio] radio already exists", rId); return; 479 | } 480 | 481 | state.globalRadios.push({ 482 | id: rId, 483 | playlist: [], 484 | currentSong: null, 485 | currentTime: 0, 486 | isPlaying: false, 487 | volume: 15, 488 | distancedVolume: 1, 489 | title: "", 490 | duration: 0, 491 | audioContext: null, 492 | audioElement: null, 493 | sourceNode: null, 494 | bassBoost: 0, 495 | trebleBoost: 0, 496 | stereoPan: 0, 497 | bassFilter: null, 498 | trebleFilter: null, 499 | stereoPanner: null, 500 | LPFilter: null, 501 | progress: "", 502 | }) 503 | 504 | debuglog("[registerRadio] radio registered", rId) 505 | } 506 | 507 | if (data.key == "syncVolume") { 508 | dispatch("updateVolume", { volume: data.args.volume, radioId: rId }) 509 | } 510 | 511 | if (data.key == "syncBassBoost") { 512 | dispatch("updateBassBoost", { bassBoost: data.args.bassBoost, radioId: rId }) 513 | } 514 | 515 | if (data.key == "syncTrebleBoost") { 516 | dispatch("updateTrebleBoost", { trebleBoost: data.args.trebleBoost, radioId: rId }) 517 | } 518 | 519 | if (data.key == "syncSeekTo") { 520 | dispatch("updateCurrentTime", { time: data.args.time, radioId: rId }) 521 | } 522 | 523 | if (data.key == "syncPause") { 524 | dispatch("syncPause", { isPlaying: data.args.isPlaying, time: data.args.time, radioId: rId }) 525 | } 526 | 527 | if (data.key == "playSong") { 528 | dispatch("playSong", { songId: data.args.songId, radioId: rId }) 529 | } 530 | } 531 | 532 | if (data.type == "updatePosition") { 533 | const radio = await dispatch("getRadioById", data.netId) 534 | if (radio) { 535 | radio.leftBalance = data.leftBalance.toFixed(4) 536 | radio.rightBalance = data.rightBalance.toFixed(4) 537 | radio.distancedVolume = data.volume 538 | 539 | const steropan = (radio.leftBalance - radio.rightBalance) / 5 540 | if (radio.stereoPanner) { 541 | radio.stereoPan = -steropan 542 | radio.stereoPanner.pan.value = radio.stereoPan 543 | } 544 | 545 | if (radio.audioElement) { 546 | radio.audioElement.volume = (radio.volume / 100) * radio.distancedVolume 547 | } 548 | 549 | if (data.LPFValue) { 550 | const value = data.LPFValue 551 | // value = min 0, max 0.5 552 | // calculate percent 553 | const valuePercent = ((value * 100) / 0.5) / 70 554 | const freq = Math.min(20000 / valuePercent, 20000) 555 | 556 | if (radio.LPFilter) { 557 | radio.LPFilter.frequency.value = freq 558 | } 559 | } else if (radio.LPFilter) { 560 | radio.LPFilter.frequency.value = 20000 561 | } 562 | } 563 | } 564 | 565 | if (data.type == "syncRadios") { // disabled 566 | const radios = data.radios 567 | debuglog("SYNC [registerRadio] radios", radios) 568 | for (let radioId in radios) { 569 | if (Object.hasOwnProperty.call(radios, radioId)) { 570 | radioId = parseInt(radioId) 571 | const radio = radios[radioId]; 572 | state.globalRadios.push({ 573 | id: radioId, 574 | playlist: radio.playlist || [], 575 | currentSong: radio.currentSong || "", 576 | currentTime: radio.currentTime || "", 577 | isPlaying: radio.isPlaying || false, 578 | volume: radio.volume || 15, 579 | distancedVolume: 1, 580 | title: radio.title || "", 581 | duration: radio.duration || 0, 582 | audioContext: null, 583 | audioElement: null, 584 | sourceNode: null, 585 | bassBoost: radio.bassBoost || 0, 586 | trebleBoost: radio.trebleBoost || 0, 587 | stereoPan: 0, 588 | bassFilter: null, 589 | trebleFilter: null, 590 | stereoPanner: null, 591 | LPFilter: null, 592 | progress: "", 593 | }) 594 | debuglog("SYNC [registerRadio] radio registered", radio, state.globalRadios) 595 | 596 | debuglog("[registerRadio] radio synced", radioId) 597 | } 598 | } 599 | } 600 | 601 | if (data.type == "config") { 602 | state.maxVolume = data.maxVolume 603 | state.bassRange = data.bassRange 604 | state.trebleRange = data.trebleRange 605 | } 606 | 607 | if (data.type == "debug") { 608 | DEBUG = data.debug 609 | } 610 | 611 | if (data.type == "vehicleEnteredScope") { 612 | if (!state.vehiclesInScope.includes(data.netId)) { 613 | state.vehiclesInScope.push(data.netId) 614 | } 615 | const radio = data.data 616 | state.globalRadios.push({ 617 | id: data.netId, 618 | playlist: radio.playlist || [], 619 | currentSong: radio.currentSong || "", 620 | currentTime: radio.currentTime || "", 621 | isPlaying: radio.isPlaying || false, 622 | volume: radio.volume || 15, 623 | distancedVolume: 1, 624 | title: radio.title || "", 625 | duration: radio.duration || 0, 626 | audioContext: null, 627 | audioElement: null, 628 | sourceNode: null, 629 | bassBoost: radio.bassBoost || 0, 630 | trebleBoost: radio.trebleBoost || 0, 631 | stereoPan: 0, 632 | bassFilter: null, 633 | trebleFilter: null, 634 | stereoPanner: null, 635 | LPFilter: null, 636 | progress: "", 637 | }) 638 | 639 | if (radio) { 640 | dispatch("createAudioSource", { 641 | radioId: data.netId, 642 | songId: radio.currentSong 643 | }) 644 | 645 | const syncedRadio = await dispatch("getRadioById", data.netId) 646 | if (syncedRadio && syncedRadio.audioElement) { 647 | debuglog("[vehicleEnteredScope] radio synced", syncedRadio, radio) 648 | 649 | dispatch("syncPause", { 650 | isPlaying: radio.isPlaying, 651 | radioId: data.netId 652 | }) 653 | 654 | dispatch("updateVolume", { 655 | volume: radio.volume, 656 | radioId: data.netId 657 | }) 658 | 659 | dispatch("updateBassBoost", { 660 | bassBoost: radio.bassBoost, 661 | radioId: data.netId 662 | }) 663 | 664 | dispatch("updateTrebleBoost", { 665 | trebleBoost: radio.trebleBoost, 666 | radioId: data.netId 667 | }) 668 | 669 | dispatch("updateCurrentTime", { 670 | time: radio.currentTime, 671 | radioId: data.netId 672 | }) 673 | } 674 | } 675 | } 676 | 677 | if (data.type == "vehicleLeftScope") { 678 | state.vehiclesInScope = state.vehiclesInScope.filter(vehicle => vehicle != data.netId) 679 | 680 | const radio = await dispatch("getRadioById", data.netId) 681 | if (radio) { 682 | if (radio.audioElement) { 683 | radio.audioElement.pause() 684 | radio.audioElement.src = "" 685 | radio.audioElement.volume = 0 686 | radio.audioElement = null 687 | } 688 | if (radio.audioContext) { 689 | radio.audioContext.close() 690 | radio.audioContext = null 691 | } 692 | if (radio.sourceNode) { 693 | radio.sourceNode = null 694 | } 695 | } 696 | } 697 | }); 698 | 699 | window.addEventListener("keydown", (e) => { 700 | if (e.key == "Escape") { 701 | post("display", { 702 | display: false 703 | }) 704 | store.state.displayPlaylist = false 705 | store.state.displayAddSong = false 706 | } 707 | }) 708 | 709 | window.onload = () => { 710 | debuglog("onload") 711 | post("requestRadioSync") 712 | } 713 | 714 | export default store --------------------------------------------------------------------------------