> For the complete documentation index, see [llms.txt](https://docs.0resmon.org/0resmon/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.0resmon.org/0resmon/wais-resoucres/wais-televisions-and-smarttv/overview.md).

# Overview

***

## 📺 Televisions & SmartTVs are everywhere!

Place a **TV anywhere** you like using the “item” feature, or **create static TVs** from the **admin menu.**

***

## 🔮 Supporting Platforms

In total, <mark style="color:$success;">**8 different popular video**</mark>**&#x20;and&#x20;**<mark style="color:$success;">**streaming**</mark> platforms are **supported in a synchronized** manner.

* **YouTube**
* **TikTok**
* **Kick**
* **Twitch**
* **Medal**
* **Streamable**
* **Vimeo**
* **SoundCloud**

***

## 💼Job integration

You can make it possible for **people in the jobs** of your choice to **control the televisions**!

You can **specify one** or **more job definitions**.

***

## ♻️ Sync & OneSync Compatibility

Watch videos or live streams in <mark style="color:$success;">**sync with**</mark> all **other players** at the <mark style="color:$success;">**same time**</mark>.

If you disconnect from the server or encounter an issue, <mark style="color:$success;">continue watching in sync with other players.</mark>

***

## 📺 Televisions

A total of <mark style="color:$success;">**20 different screens**</mark>

* 2 monitors
* <mark style="color:yellow;">3 vintage</mark>
* <mark style="color:yellow;">4 CRT</mark>
* <mark style="color:orange;">**4 cinematic / large**</mark>
* <mark style="color:red;">**7 flat-screen**</mark>

A flexible and **dynamic structure** that allows you to **add more screens!**

***

## 🪄 User-Friendly Menu (UI - User Interface)

Manage your video enjoyment with a compact and **simple interface!**

View and play **your recently played videos/streams!**

**Save your favorite videos/streams** and play them whenever you want!

***

## ⚙️ Performance

<mark style="color:$success;">**NUI (CEF)**</mark>: The JavaScript Instance allocates an average of 200KB of RAM for a new video/stream.&#x20;

* Note: *All YouTube operations are performed entirely by another server, <mark style="color:$success;">**so it does not cause issues like NUI (CEF) crashes!**</mark>*

<mark style="color:$success;">**Client Side**</mark>: Efforts have been made to optimize the system as much as possible. Unnecessary DUI elements are not rendered. The values are as follows:

* **Idle 0.00 ms**
* **0.03 ms when setting up a TV**
* **0.00 ms when near a TV**. **0.03 ms if a video is playing**

<mark style="color:$success;">**Server Side**</mark>**:** Generally **0.00 ms**, and **0.01 ms** for any **video loading process**.

***

## 🚫 Blacklisted locations

**Televisions cannot be installed in these areas. No television-related activities are allowed.**

***

## 🚩Supporting 7 Languages / You can add more

Currently, ar, cz, ro, it, fr, de, and tr language support is available. You can add more from the locale files.

***

## Config Files

<details>

<summary>Config</summary>

```lua
Config = {}
Config.Framework = {
    ["Framework"] = "auto", -- [[LEAVE AUTO]] auto, esx, qbcore or qbox
    ["ResourceName"] = "auto", -- [[LEAVE AUTO]] auto, es_extended or qb-core or your resource name. If you using qbx you should write qb-core
    ["SharedEvent"] = "" -- Event name for old cores.
}

Config.Language = "en" -- ar, cz, ro, it, fr, de, tr
Config.ShowDebug = false -- Show debug messages
Config.AdminMenu = {
    ["command"] = "tvadmin", -- Command to open the admin menu
    ["ace"] = "group.admin" -- ACE permission for the admin menu
}
Config.TvCommand = {
    ["command"] = "tv", -- This allows you to open the menu on your TV.
    ["suggestion"] = "This allows you to open the menu on the nearby TV" -- Command suggestion for the tv command
}
Config.DeleteTvWhenOwnerQuit = false --[[
    ⚠️⚠️⚠️
    Its recommended to keep it true, if you set it to false, when the owner of the TV leaves the game, 
    the TV will remain in the world and other players can use it. 
    
    If you set it to true, when the owner of the TV leaves the game,
     the TV will be deleted from the world.
]]
Config.SyncCheck = true --[[
    ⚠️⚠️⚠️
    This variable belongs to the code structure that checks whether the video is progressing as it should, in sync with other people.
    The reasons I use this are as follows:
        - When the game is minimized, the CPU freezes all game-related processes for a certain number of seconds or minutes. 
            > This suspends the game's processes. When the player returns to the game, they will hear the videos behind schedule by the duration of the freeze compared to other players. 
        - Players experiencing occasional freezes or lag will hear the video delayed by the duration of the game's lag. 
    This way, the remaining users will move back to the current second.
    
    
    💢💢💢
    If you set it to `false`, when these conditions occur, the user's video will lag behind the others (by the amount of time it freezes or stutters). 
    Therefore, it will not be synchronized, and when the video ends at the expected second, it will suddenly cut off. 
]]

Config.UsableItems = {
    ["tv_crt_classic"] = {
        ["model"] = "prop_tv_01",
        ["screen"] = "tvscreen",
        ["label"] = "Classic CRT TV"
    },
    ["tv_crt_old"] = {
        ["model"] = "prop_tv_03",
        ["screen"] = "tvscreen",
        ["label"] = "Old CRT TV"
    },
    ["tv_crt_wood"] = {
        ["model"] = "prop_tv_06",
        ["screen"] = "tvscreen",
        ["label"] = "Wooden CRT TV"
    },
    ["tv_flat_small"] = {
        ["model"] = "prop_tv_flat_03b",
        ["screen"] = "tvscreen",
        ["label"] = "Small Flat TV"
    },
    ["tv_cinema"] = {
        ["model"] = "vw_prop_vw_cinema_tv_01",
        ["screen"] = "tvscreen",
        ["label"] = "Cinema TV"
    },
    ["tv_curved_xl"] = {
        ["model"] = "xm_prop_x17_screens_02a",
        ["screen"] = "tvscreen",
        ["label"] = "Curved XL TV"
    },
    ["tv_smash"] = {
        ["model"] = "des_tvsmash_start",
        ["screen"] = "tvscreen",
        ["label"] = "Smash TV"
    },
    ["tv_flat_overlay"] = {
        ["model"] = "prop_flatscreen_overlay",
        ["screen"] = "tvscreen",
        ["label"] = "Flat Overlay TV"
    },
    ["tv_laptop"] = {
        ["model"] = "prop_laptop_lester2",
        ["screen"] = "tvscreen",
        ["label"] = "Laptop TV"
    },
    ["tv_trevor"] = {
        ["model"] = "prop_trev_tv_01",
        ["screen"] = "tvscreen",
        ["label"] = "Trevor TV"
    },
    ["tv_vintage"] = {
        ["model"] = "prop_tv_02",
        ["screen"] = "tvscreen",
        ["label"] = "Vintage TV"
    },
    ["tv_crt_overlay"] = {
        ["model"] = "prop_tv_03_overlay",
        ["screen"] = "tvscreen",
        ["label"] = "CRT Overlay TV"
    },
    ["tv_flat_classic"] = {
        ["model"] = "prop_tv_flat_01",
        ["screen"] = "tvscreen",
        ["label"] = "Flat Classic TV"
    },
    ["tv_flat_screen"] = {
        ["model"] = "prop_tv_flat_01_screen",
        ["screen"] = "tvscreen",
        ["job"] = {"police", "ambulance"},
        ["label"] = "Flat Screen TV"
    },
    ["tv_flat_medium"] = {
        ["model"] = "prop_tv_flat_02b",
        ["screen"] = "tvscreen",
        ["label"] = "Flat Medium TV"
    },
    ["tv_flat_modern"] = {
        ["model"] = "prop_tv_flat_03",
        ["screen"] = "tvscreen",
        ["label"] = "Flat Modern TV"
    },
    ["tv_flat_premium"] = {
        ["model"] = "prop_tv_flat_michael",
        ["screen"] = "tvscreen",
        ["label"] = "Flat Premium TV"
    },
    ["tv_monitor_large"] = {
        ["model"] = "prop_monitor_w_large",
        ["screen"] = "tvscreen",
        ["label"] = "Large Monitor TV"
    },
}

Config.PlaceOptions = {
    ["timeout"] = 15 * 1000, -- Time to wait for the player to place the TV 
    ["maxDistance"] = 15.0, -- Max distance to place the TV from the player
    ["buttons"] = {
        ["accept"] = "E", -- Button to accept placing the TV
        ["cancel"] = "G", -- Button to cancel placing the TV
        ["snapGround"] = "Z" -- Button to snap the TV to the ground.
    },
    ["offset"] = vector4(1.25, 0.0, 0.0, 0.0), -- Offset from the player when placing the TV (x, y, z, heading)
    ["animations"] = {
        ["dict"] = "anim@heists@load_box", -- Animation dictionary for placing the TV 
        ["clip"] = "load_box_1", -- Animation clip for placing the TV
        ["flag"] = 1, -- Animation flag for placing the TV
        ["duration"] = 2.5 * 1000, -- Duration of the animation in milliseconds
    },
}

Config.BlacklistedCoords = {
    --[[
        🚫🚫🚫
        You can add blacklist coordinates so that people cannot place tv's or screens there. 
        Example: { coords = vector3(x, y, z), radius = 10.0 }
    ]]

    { coords = vector3(455.5057, -994.2595, 26.9876), radius = 45.0 }, -- Mission Row Police Department [MRPD]
    { coords = vector3(320.7686, -590.9871, 45.0281), radius = 40.0 } -- Pillbox Hill Medical Center [PHMC]
}

Config.Debug = function(...)
    if Config.ShowDebug then
        print(...)
    end
end

Config.Notification = function(title, text, type, time)
    title = title or "Television"
    time = time or 5000

    return lib.notify({
        title = title,
        type = type,
        duration = time,
        description = text,
        iconAnimation = "beatFade",
        position = "top"
    })
end
```

</details>

<details>

<summary>Server Config</summary>

```lua
ConfigSv = {}
ConfigSv.Webhook = "" -- Webhook URL for logging when a song starts playing. Set this to your Discord webhook URL to enable logging.
ConfigSv.Inventorys = {
    -- [[ 🟢 Inventory detections, to work automatically compatible with some inventories. 🟢 ]]
    ["qb_inventory"] = GetResourceState("qb-inventory"):find("start") and true or false,
    ["qs_inventory"] = GetResourceState("qs-inventory"):find("start") and true or false,
    ["ox_inventory"] = GetResourceState("ox_inventory"):find("start") and true or false,
    ["export"] = nil
}

---------------------------------------------------------------------------------------------------------------
---This function is used to access player names.
---@enum [parent=#ConfigSv] GetPlayerName
---@param source
---@return #string
ConfigSv.GetPlayerName = function(source)
    if Config.Framework.Framework == "esx" then
        local Player = wFramework.Framework.GetPlayerFromId(source)
        return Player ~= nil and ('%s - %s'):format(Player.get('firstName'), Player.get('lastName')) or "Unknown Player"
    elseif Config.Framework.Framework:find("qb") then
        local Player = wFramework.Framework.Functions.GetPlayer(source)
        return Player ~= nil and ('%s - %s'):format(Player.PlayerData.charinfo.firstname, Player.PlayerData.charinfo.lastname) or "Unknown Player"
    end
end
---------------------------------------------------------------------------------------------------------------

---------------------------------------------------------------------------------------------------------------
---Function that takes care of player items giving
---@enum [parent=#ConfigSv] AddItem
---@param source
---@param item
---@param amount
---@return #boolean
ConfigSv.AddItem = function(src, item, amount)
    local p = promise:new()

    if Config.Framework.Framework == "esx" then
        local xPlayer = wFramework.Framework.GetPlayerFromId(src)
        if xPlayer ~= nil then
            xPlayer.addInventoryItem(item, amount)
            p:resolve(true)
        else
            Config.Debug(("[^1ERROR - ADDITEM^0] Could not find player with source %s - ConfigSv.AddItem"):format(src))
            p:resolve(false)
        end
    elseif Config.Framework.Framework:find("qb") then
        local Player = wFramework.Framework.Functions.GetPlayer(src)
        if Player ~= nil then
            if (ConfigSv.Inventorys.export == nil or ConfigSv.Inventorys.export:AddItem(src, item, amount) == nil) then
                Player.Functions.AddItem(item, amount)
            end

            p:resolve(true)
        else
            Config.Debug(("[^1ERROR - ADDITEM^0] Could not find player with source %s - ConfigSv.AddItem"):format(src))
            p:resolve(false)
        end
    else
        Config.Debug(("[^1ERROR - ADDITEM^0] No compatible inventory found for adding item to player with source %s - ConfigSv.AddItem"):format(src))
        p:resolve(false)
    end

    return Citizen.Await(p)
end
---------------------------------------------------------------------------------------------------------------

---------------------------------------------------------------------------------------------------------------
---Function that takes care of player items removing
---@enum [parent=#ConfigSv] RemoveItem
---@param source
---@param item
---@param amount
---@return #boolean
ConfigSv.RemoveItem = function(src, item, amount)
    local p = promise:new()
    local Player = Config.Framework.Framework == "esx" and wFramework.Framework.GetPlayerFromId(src) or ((Config.Framework.Framework:find("qb")) and wFramework.Framework.Functions.GetPlayer(src) or nil)
    if not Player then
        p:resolve(false)
        Config.Debug(("[^1ERROR - REMOVEITEM^0] Could not find player with source %s - ConfigSv.RemoveItem"):format(src))
        return Citizen.Await(p)
    end

    if ConfigSv.Inventorys.export ~= nil then
        p:resolve(ConfigSv.Inventorys.export:RemoveItem(src, item, amount))
        return Citizen.Await(p)
    end

    if Config.Framework.Framework:find("qb") then
        p:resolve(Player.Functions.RemoveItem(item, amount))
    else
        if Player.getInventoryItem(item).count >= amount then
            Player.removeInventoryItem(item, amount)
            p:resolve(true)
        else
            p:resolve(false)
        end
    end

    return Citizen.Await(p)
end
---------------------------------------------------------------------------------------------------------------

---Function to send a webhook when a song starts playing. It sends the owner's source, video ID, net ID, and metadata (which includes title, length, author, etc.). You can customize the data sent to the webhook as needed.
---@enum [parent=#ConfigSv] SendWebhook
---@param owner number The source of the player who started the song.
---@param videoId string The ID of the YouTube video being played.
---@param netId number The network ID of the television entity.
---@param metaData table A table containing metadata about the song, such as title, length, author, thumbnail, etc.
---@param platform string The platform of the video/stream (e.g., "youtube", "twitch").
---@param url string The URL of the video/stream.
---@return void
ConfigSv.SendWebhook = function(sender, coords, url, platform, metadata)
    if ConfigSv.Webhook == "" then return end
    --[[
        🟢🟢🟢
        You can set up a webhook to log when a song starts playing. 
        The function receives the owner's source, video ID, net ID, and metadata (which includes title, length, author, etc.). 
        You can customize the data sent to the webhook as needed.
    ]]
    
    local ts = os.time()
    local time = os.date('!%Y-%m-%dT%H:%M:%S.000Z', ts)
    local name = sender > 0 and GetPlayerName(sender) or "Server DefaultTV"

    local embed = {
        {
            ["description"] = 'The player played a video/stream',
            ["color"] = 16705372,
            ["fields"] = {
                {["name"] = "📃 Player", ["value"] = ('**%s [%s]**'):format(name, sender)},
                {["name"] = "🎥 Video/Stream", ["value"] = ('**[%s-%s](%s)**'):format(metadata.author, metadata.title, CompleteURL(platform, url))},
                {["name"] = "📍 Coords", ["value"] = ("vec3(%.2f, %.2f, %.2f)"):format(coords.x, coords.y, coords.z)},
            },
            ["author"] = {
                ["name"] = "Television",
                ["url"] = "https://0resmon.tebex.io/package/fivem-speaker-boombox-carplay-script",
                ["icon_url"] = "https://cdn-icons-png.flaticon.com/32/4406/4406124.png"
            },
            ["timestamp"] =  time,
            ["thumbnail"] =  {
                ["url"] =  metadata.thumbnail
            }
        }
    }
    PerformHttpRequest(ConfigSv.Webhook, function(err, text, headers) end, 'POST', json.encode({username = "wais-tv", avatar_url = "https://cdn.discordapp.com/avatars/225154669817626626/12042ba04def3b74fdb3f6492853ee37.png?size=1024", embeds = embed}), { ['Content-Type'] = 'application/json' })
end
---------------------------------------------------------------------------------------------------------------

CreateThread(function()
    ConfigSv.Inventorys.export = ConfigSv.Inventorys.qs_inventory and exports['qs-inventory'] or ConfigSv.Inventorys.ox_inventory and exports.ox_inventory or nil or ConfigSv.Inventorys.qb_inventory and exports['qb-inventory'] or nil
    if not ConfigSv.Inventorys.export then
        Config.Debug("No compatible inventory found, please set the export method manually in config_sv.lua")
    end
end)
```

</details>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.0resmon.org/0resmon/wais-resoucres/wais-televisions-and-smarttv/overview.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
