├── 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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
9 |
10 |
11 | Treble
12 |
13 |
14 |
15 |
16 |
17 |
18 |
62 |
63 |
--------------------------------------------------------------------------------
/[src]/src/components/controllers/Timeline.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ startTime }}
5 |
6 |
7 |
8 | {{ endTime }}
9 |
10 |
11 |
12 |
13 |
79 |
80 |
--------------------------------------------------------------------------------
/[src]/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
66 |
67 |
--------------------------------------------------------------------------------
/[src]/src/components/AddSong.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
15 |
16 | Add
17 |
18 |
19 | {{ progress }}
20 |
21 |
22 |
23 |
24 |
68 |
69 |
--------------------------------------------------------------------------------
/[src]/src/components/Slider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
80 |
81 |
--------------------------------------------------------------------------------
/[src]/src/components/Playlist.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 | Playing now
14 |
15 |
20 |
21 | Queue
22 |
23 |
34 |
35 |
36 |
37 |
38 |
92 |
93 |
--------------------------------------------------------------------------------
/[src]/src/components/controllers/ControlButtons.vue:
--------------------------------------------------------------------------------
1 |
2 |
35 |
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
--------------------------------------------------------------------------------