Pavlov TV API
Pavlov TV has exposed endpoints to allow users to create their own UI interface for use in steaming software such as OBS, data-sets, or any other purpose of your choosing from Pavlov replays.
Enable the API
1. Ensure you have PavlovVR installed on steam and locate the GameUserSettings.ini file.
This file on windows machines will usually be found at [ %localappdata%\Pavlov\Saved\Config\Windows\GameUserSettings.ini ] You can paste this into your file explorer(without the brackets [] ) to open the file.
2. Go the the bottom portion of the [/Script/Pavlov.PavlovGameUserSettings] part of the file, ABOVE[ScalabilityGroups].
3. Add PavTVAPIPort=1234 onto the file at that location. 1234 is the port that will be used, if you know this port is in use already, please find another on your device that is not in use and list it here.
4. Run PavTV
Using OBS with your generated/written HTML file
1. Copy your HTML file path
2. Go to “Sources” and click the “+” button
3. Add browser source and create new (name it whatever)
4. Adjust Resolution of the source(recording size)
5. Paste in the HTML file path and click OK
The GET Endpoints
*If you are new to this, you are able to put any of these endpoint “links” into your browser or run curl requests and check that they are functional
MatchStatus
Ex.in JS to fetch this endpoint on port 1234 const response = await fetch('http://localhost:1234/MatchStatus'); const matchData = await response.json()
const response = await fetch('http://localhost:1234/MatchStatus');
const matchData = await response.json();
Elements in MatchStatus
MatchActive- Will return whether a match is currently ongoing or not
Ex. “MatchActive”: true or “MatchActive”: false
RoundTime- Will return the active time into the round in seconds
Ex. "RoundTime": 105
Teams- Will return true/false if the there are teams in this match
Ex. "Teams": true
RoundsLeft- Will return the maximum number of rounds left in the match
Ex. "RoundsLeft": 15
Team0Score- Will return blue team’s score
Team1Score- Will return red team’s score
Ex. "Team0Score": 1,
"Team1Score": 2,
AttackingTeamId- Will return the ID of the currently attacking team 0-blue, 1- red
Ex. "AttackingTeamId": 1
Team0Cash- Will return blue team’s collective cash
Team1Cash- Will return red team’s collective cash
Ex. "Team0Cash": 2500, "Team1Cash": 2500 MatchStatus.Team0 or MatchStatus.Team1
MatchStatus.Team0 or MatchStatus.Team1
Ex.in JS to fetch Team0 from MatchStatus
const teamZero = matchData.Team0
const teamOne = matchData.Team1
These will give you an array of JSON style objects
"Team0": [
{ "Name": "JaredOne", "Cash": 1500, "Score": 8, "Kills": 3, "Deaths": 2, "Dead": true, "Health": 0, "Armour": 0, "Helmet": false, "PrimaryWeapon": "https://test-pav-tv.vankrupt.net/icons/m16.png", "SecondaryWeapons": [ "https://test-pav-tv.vankrupt.net/icons/knife.png" ], "Avatar": "http://prod.cdn.pavlov-vr.com/avatar/ThisTotallyExistsZero.png", "Bot": false }, { "Name": "JaredTwo", "Cash": 150, "Score": 6, "Kills": 3, "Deaths": 2, "Dead": false, "Health": 100, "Armour": 0, "Helmet": false, "PrimaryWeapon": "https://test-pav-tv.vankrupt.net/icons/m16.png", "SecondaryWeapons": [ "https://test-pav-tv.vankrupt.net/icons/knife.png" ], "Avatar": "http://prod.cdn.pavlov-vr.com/avatar/ThisTotallyExistsOne.png", "Bot": false }, |
You can then grab subsequent information from the teams like this
matchData.Team0[0].Name
This would return the name in string form from the first player of Team0. You can do this with all of the information relating to the player, players are returned in order of score highest to lowest [as how it would show up in scoreboard]
For the values in each player:
Name returns a String
Cash, Score, Kills, Deaths, Health, and Armor are all returning Integer values
Dead, Helmet, and Bot return Bool
PrimaryWeapon, SecondaryWeapon, and Avatar all return strings containing links to image files that can be used in your scoreboard.
Killfeed GET Endpoint
Gives you a live killfeed of the match you are watching Ex. in Js of how you would get this endpoint as a Json object
const response = await fetch('http://localhost:1234/Killfeed');
const killfeedData = await response.json();
Returns the most recent
"Killfeed": [
{ "Killer": "jaredOne", "Killed": "jaredTwo", "Headshot": true, "EntryLifespan": 3.2541036605834961, "KilledBy": "Tokarev", "KillerTeam": 0, "KilledTeam": 1 } ] |
Ex of how you would get any of the following for the first object in the killfeed
killfeedData.killfeed[0].killer
Killer and Killed return String objects of the killer and killed respectively
Headshot returns whether the death was caused by a headshot, in the form of a bool
EntryLifespan returns the time left counting down from 4 seconds in a float/number
KilledBy returns the weaponId of the killing weapon in a string
KillerTeam and KilledTeam returns either 0(blue) or 1(red) team of the Killer as an Integer
MatchEvents GET Endpoint
Gives you an event log for things like Win and Round, will list ALL events during the round if watching a replay, will list them as they happen if watching live
const response = await fetch('http://localhost:1234/MatchEvents'); const eventData = await response.json();
{
"MatchTime": 48.085689544677734, "TotalTime": 80.008003234863281, "Events": [ { "Name": "Round", "Time": 15, "AdditionalData": 1 }, { "Name": "Win", "Time": 31, "AdditionalData": 1 } ] } |
Name gives the event name in a string being either “Round”, “Win”, “Planted”
Time gives the time in seconds in integer form of the event going into the game [will list events from start to end]
AdditionalData will be dependent for the event type
-For a “Win” event 0 will represent Team0 victory, 1 will represent Team1 victory
-For a “Round” event the number returned will represent what round it is
-For a “Planted” event the number does not matter it will always be 0
Camera Paths
Steps to Use:
1. Go into a map and record a camera path for keys [1], [2], [3] , or [4], using LCtrl + [Key] The path will be saved to the api endpoint located here:
http://localhost:1234/Camera/Path/#
NOTE: Path values start at 0, so your [1] key will be Path 0, [2] is Path 1, [3] is Path 2, and [4] is Path 3.
NOTE 2: Camera paths are deleted in between matches so make sure to save them before exiting
3. You can play the path with LShift + [Key]
Camera Path GET Endpoint - Retrieve saved camera path
Javascript example of how to use it
NOTE: Replace the “#” with the number path corresponding above 0, 1, 2, 3
const response = await fetch('http://localhost:1234/Camera/Path/#');
const cameraPath = await response.json();
Example of returned path with a single point
{"MapName":"Oilrig","PathNumber":0,"Points":[{"Time":0,"X":7453.70035785 53522,"Y":4584.0434961287474,"Z":1223.5857404802396,"Pitch":-4.375000059 6046359,"Yaw":-155.3252139880791,"Roll":0}]}
MapName gives you the name of the map of the current match you are in as a string
PathNumber is the same as the path you put in to retrieve it as an integer
Time gives you the time in seconds as a float
X,Y,Z give you the relative x-plane, y-plane, z-plane position each in float
Pitch, Roll, Yaw give you the orientation of the camera relative to the world normal, each in float
Camera Play GET Endpoint - It will call your game to play the path currently in that slot
Ex. of fetching it in Js, since there is no response, you can just run fetch
fetch('http://localhost:1234/Camera/Play/0')
This is the same principle as getting the path, though there will be no response to check it will just trigger immediately in your client and you will go along the active path bound to that key.
Camera Path POST Endpoint - Insert your path into the live slot
Ex. of a javascript POST fetch request to the api
const response = await fetch(`localhost:1234/Camera/Path/1`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
Notes: MapName and PathNumber do NOT matter in terms of POST-ing a path, so you do not have to modify those at all. The only thing that really matters is the points. It is recommended to store paths in json files for modification and accessibility ease.
GET /Pause
returns
{
"Paused": true
}
POST /Pause
{
"Paused": false
}
returns
{
"Successful": true
}
if successful returns false it will also include a string field Reason
explaining why it failed
GET /MatchTime
returns
{
"MatchTime": 19.035182
}
POST /MatchTime
returns
{
"MatchTime": 120.5
}
if successful returns false it will also include a string field Reason
explaining why it failed GET /ReplayList
returns
{
"Replays": [
{
"Id": "Sand-DM-2024.11.15-01.44.05",
"Name": "Sand",
"GameMode": "DM",
"Timestamp": "2024-11-15T01:44:05.687Z",
"Shack": true,
"LocalReplay": true,
"Live": false
},
{
"Id": "ac8d018f56f3f820033d319af942be3e",
"Name": "Sand",
"GameMode": "DM",
"Timestamp": "2024-11-27T08:28:37.362Z",
"Shack": false,
"LocalReplay": false,
"Live": false
}
]
}
POST /LoadReplay
{
"Id": "ac8d018f56f3f820033d319af942be3e"
}
return
{
"Successful": true
}
/PlayerPos
endpoint to include these fields in SND if the bomb is available. Bomb States (0 = dropped, 1 = carried, 2 = planted)
"BombX": -3479.5815804967287,
"BombY": -144.55990951728171,
"BombZ": -262.77667585565382,
"BombState": 1
Possible issues you may encounter
TypeError: fetch failed - cause: Error: connect ECONNREFUSED:
This error happens because you are likely running your javascript function with nodejs or the like and it is trying to force ipv6, ensure it does ipv4 by changing “localhost” to “127.0.0.1”. When pulling from the web source it will not matter since it understands the difference. Newer versions of NodeJS should not have this problem and be able to determine between ipv4 and ipv6. (Version 19.3 or higher)
Refused to connect
If “http:localhost:1234” does not show anything or “refused to connect” the service is not running on this port and you will need to try to set up the API again. Ensure you have PavTVAPIPort=1234 inside of your gameusersettings.ini