├── README.md ├── LICENSE └── CSGOPlayback.py /README.md: -------------------------------------------------------------------------------- 1 | # OBS CS2自动回放脚本(OBS CS2 Auto-playback Script) 2 | 3 | [video](https://github.com/Krash220/OBS-CS2-AutoPlayback/assets/121000471/20ceb0b7-89a3-40d6-af5d-298cb5a16ef2) 4 | 5 | 使用方法: 6 | 1. 在捕获CS2的场景内新增媒体源 7 | 2. 安装Python3.10或更低版本(不能是微软商店的) 8 | 3. 加载脚本,设置媒体源和其所在的场景 9 | 4. 将输出录制的关键帧设为1s,开启回放缓存 10 | 5. 启动回放缓存功能,打开游戏,开始使用 11 | 12 | 您需要手动为媒体源设置显示转场和隐藏转场,本仓库不提供视频中的转场效果。 13 | 14 | Usage: 15 | 1. Add a new media source within the scene capturing CS2 16 | 2. Install Python 3.10 or lower (not from the Microsoft Store). 17 | 3. Load the script, set up the media source and the scene it is in. 18 | 4. Set the output recorded keyframes to 1s and turn on replay buffering. 19 | 5. Activate the replay buffering, enjoy your game and realtime playback. 20 | 21 | You need to manually set show transitions and hide transitions for the media source, this repository does not provide transitions effects in videos. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Krash220 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 | -------------------------------------------------------------------------------- /CSGOPlayback.py: -------------------------------------------------------------------------------- 1 | import obspython as obs 2 | import winreg 3 | import os 4 | import random 5 | import base64 6 | import json 7 | import threading 8 | import time 9 | from http import HTTPStatus 10 | from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer 11 | 12 | scene_name = None 13 | source_name = None 14 | 15 | csi_path = None 16 | http_server = None 17 | token = None 18 | 19 | event_stack = [] 20 | kill_type = [] 21 | playback_list = [] 22 | last_round = None 23 | update_time = False 24 | 25 | def update_playback_list(): 26 | if kill_type: 27 | fn = obs.obs_frontend_get_last_replay() 28 | if fn and fn != last_round: 29 | hs = kill_type.pop(0) 30 | playback_list.append(fn) 31 | 32 | obs.script_log(obs.LOG_INFO, 'Added Playback: %s %s' % (str(hs), fn)) 33 | 34 | if hs: 35 | playback_list.append(fn) 36 | playback_list.append(fn) 37 | 38 | def update_last_round(): 39 | global last_round 40 | last_round = obs.obs_frontend_get_last_replay() 41 | 42 | def save_playback(): 43 | update_playback_list() 44 | 45 | obs.obs_frontend_replay_buffer_save() 46 | 47 | def start_playback(): 48 | global update_time 49 | update_playback_list() 50 | 51 | if playback_list: 52 | fn = playback_list[random.randint(0, len(playback_list) - 1)] 53 | obs.script_log(obs.LOG_INFO, 'Choose Playback: %s' % fn) 54 | obs.script_log(obs.LOG_INFO, 'Playbacks: %s' % str(playback_list)) 55 | playback_list.clear() 56 | 57 | scene = obs.obs_get_scene_by_name(scene_name) 58 | source = obs.obs_get_source_by_name(source_name) 59 | 60 | if scene and source: 61 | settings = obs.obs_source_get_settings(source) 62 | 63 | obs.obs_data_set_string(settings, 'local_file', fn) 64 | obs.obs_data_set_bool(settings, 'restart_on_activate', True) 65 | obs.obs_data_set_bool(settings, 'clear_on_media_end', False) 66 | obs.obs_data_set_int(settings, 'speed_percent', 33) 67 | obs.obs_source_update(source, settings) 68 | 69 | obs.obs_data_release(settings) 70 | 71 | item = obs.obs_scene_sceneitem_from_source(scene, source) 72 | 73 | obs.obs_sceneitem_set_visible(item, True) 74 | update_time = True 75 | 76 | obs.obs_sceneitem_release(item) 77 | obs.obs_source_release(source) 78 | obs.obs_source_release(obs.obs_scene_get_source(scene)) 79 | 80 | def stop_playback(): 81 | scene = obs.obs_get_scene_by_name(scene_name) 82 | source = obs.obs_get_source_by_name(source_name) 83 | 84 | if scene and source: 85 | item = obs.obs_scene_sceneitem_from_source(scene, source) 86 | 87 | obs.obs_sceneitem_set_visible(item, False) 88 | 89 | obs.obs_sceneitem_release(item) 90 | obs.obs_source_release(source) 91 | obs.obs_source_release(obs.obs_scene_get_source(scene)) 92 | 93 | class CSGSIServer(SimpleHTTPRequestHandler): 94 | def do_GET(self): 95 | self.send_response(HTTPStatus.OK) 96 | self.send_header("Content-Type", 'application/text') 97 | self.send_header("Content-Length", 0) 98 | self.end_headers() 99 | self.wfile.flush() 100 | 101 | def do_POST(self): 102 | global last_is_hs 103 | 104 | cs = json.loads(self.rfile.read(int(self.headers.get('content-length'))).decode()) 105 | 106 | if cs['auth']['token'] == token: 107 | prev = {} 108 | 109 | if 'previously' in cs: 110 | prev = cs['previously'] 111 | 112 | if 'player' in cs and 'map' in cs: 113 | if cs['provider']['steamid'] == cs['player']['steamid'] and cs['map']['phase'] == 'live' and cs['round']['phase'] in ['live', 'over']: 114 | last_kills = kills = cs['player']['state']['round_kills'] 115 | last_killhs = killhs = cs['player']['state']['round_killhs'] 116 | 117 | if 'player' in prev and type(prev['player']) is dict and 'state' in prev['player']: 118 | last_kills = prev['player']['state']['round_kills'] if 'round_kills' in prev['player']['state'] else kills 119 | last_killhs = prev['player']['state']['round_killhs'] if 'round_killhs' in prev['player']['state'] else killhs 120 | 121 | if killhs > last_killhs: 122 | kill_type.append(True) 123 | event_stack.append((time.time() + 1, save_playback)) 124 | elif kills > last_kills: 125 | kill_type.append(False) 126 | event_stack.append((time.time() + 1, save_playback)) 127 | 128 | if 'round' in prev and type(prev['round']) is dict and 'phase' in prev['round']: 129 | if prev['round']['phase'] == 'freezetime' and cs['round']['phase'] == 'live': 130 | kill_type.clear() 131 | event_stack.append((time.time(), update_last_round)) 132 | obs.script_log(obs.LOG_INFO, 'Clear old playback.') 133 | elif prev['round']['phase'] == 'live': 134 | if cs['map']['phase'] == 'gameover': 135 | event_stack.append((time.time() + 9.75, start_playback)) 136 | elif cs['round']['phase'] == 'over': 137 | event_stack.append((time.time() + 2.25, start_playback)) 138 | 139 | self.send_response(HTTPStatus.OK) 140 | self.send_header("Content-Type", 'application/text') 141 | self.send_header("Content-Length", 0) 142 | self.end_headers() 143 | self.wfile.flush() 144 | 145 | def log_request(self, code='-', size='-'): 146 | pass 147 | 148 | def http_thread(): 149 | global token 150 | global http_server 151 | 152 | server = ThreadingHTTPServer(('127.0.0.1', 0), CSGSIServer) 153 | server.finish_request 154 | 155 | token = base64.b64encode(random.randbytes(18)).decode() 156 | cfg = """"OBS Playback" 157 | { 158 | "uri" "http://127.0.0.1:""" + str(server.server_port) + """" 159 | "timeout" "0.1" 160 | "buffer" "0.1" 161 | "throttle" "0.5" 162 | "heartbeat" "60.0" 163 | "auth" 164 | { 165 | "token" \"""" + token + """\" 166 | } 167 | "data" 168 | { 169 | "provider" "1" 170 | "map" "1" 171 | "round" "1" 172 | "player_id" "1" 173 | "player_state" "1" 174 | } 175 | } 176 | """ 177 | 178 | if os.path.exists(csi_path + 'csgo\\cfg'): 179 | with open(csi_path + 'csgo\\cfg\\gamestate_integration_obsplayback.cfg', 'w') as fd: 180 | fd.write(cfg) 181 | 182 | if os.path.exists(csi_path + 'game\\csgo\\cfg'): 183 | with open(csi_path + 'game\\csgo\\cfg\\gamestate_integration_obsplayback.cfg', 'w') as fd: 184 | fd.write(cfg) 185 | 186 | http_server = server 187 | http_server.serve_forever() 188 | 189 | def script_tick(seconds): 190 | global update_time 191 | 192 | now = time.time() 193 | rem = [] 194 | for item in event_stack: 195 | t, call = item 196 | if now > t: 197 | if obs.obs_frontend_replay_buffer_active(): 198 | call() 199 | rem.append(item) 200 | 201 | for item in rem: 202 | event_stack.remove(item) 203 | 204 | if update_time: 205 | source = obs.obs_get_source_by_name(source_name) 206 | if source and obs.obs_source_media_get_state(source) == obs.OBS_MEDIA_STATE_PLAYING and obs.obs_source_media_get_time(source) > 50: # magic number 207 | ts = obs.obs_source_media_get_duration(source) - 2333 208 | ts = int((ts / 1000.0) + 0.5) * 1000 209 | event_stack.append((time.time() + (obs.obs_source_media_get_duration(source) - ts - 333) / 330.0, stop_playback)) 210 | obs.obs_source_media_set_time(source, ts + 100) 211 | update_time = False 212 | if source: 213 | obs.obs_source_release(source) 214 | 215 | def script_load(settings): 216 | global csi_path 217 | 218 | try: 219 | key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\Valve\Steam', 0, winreg.KEY_READ) 220 | path, _ = winreg.QueryValueEx(key, 'SteamPath') 221 | winreg.CloseKey(key) 222 | except: 223 | obs.script_log(obs.LOG_ERROR, '无法获取Steam路径,脚本无法生效。') 224 | return 225 | 226 | libpath = path + '/steamapps/libraryfolders.vdf' 227 | 228 | with open(libpath, 'r') as libvdf: 229 | library = libvdf.readlines() 230 | last_path = None 231 | found = False 232 | 233 | for line in library: 234 | pair = line.strip('\n').strip('\t') 235 | if pair.startswith('"path"'): 236 | _, v = pair.split('\t\t') 237 | last_path = v.strip('"').encode().decode('unicode_escape') 238 | elif '"730"' in pair: 239 | found = True 240 | break 241 | 242 | if found and os.path.exists(last_path + '\\steamapps\\common\\Counter-Strike Global Offensive\\'): 243 | csi_path = last_path + '\\steamapps\\common\\Counter-Strike Global Offensive\\' 244 | else: 245 | obs.script_log(obs.LOG_ERROR, '无法获取CSGO游戏路径,脚本无法生效。') 246 | return 247 | 248 | threading.Thread(target=http_thread).start() 249 | 250 | def script_unload(): 251 | if http_server: 252 | http_server.shutdown() 253 | 254 | if os.path.exists(csi_path + 'csgo\\cfg\\gamestate_integration_obsplayback.cfg'): 255 | os.remove(csi_path + 'csgo\\cfg\\gamestate_integration_obsplayback.cfg') 256 | if os.path.exists(csi_path + 'game\\csgo\\cfg\\gamestate_integration_obsplayback.cfg'): 257 | os.remove(csi_path + 'game\\csgo\\cfg\\gamestate_integration_obsplayback.cfg') 258 | 259 | def script_update(settings): 260 | global scene_name 261 | global source_name 262 | 263 | scene_name = obs.obs_data_get_string(settings, 'scene') 264 | source_name = obs.obs_data_get_string(settings, 'source') 265 | 266 | def script_properties(): 267 | props = obs.obs_properties_create() 268 | 269 | p_scene = obs.obs_properties_add_list(props, 'scene', 'Scene', obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) 270 | p_source = obs.obs_properties_add_list(props, 'source', 'Source', obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) 271 | 272 | scenes = obs.obs_frontend_get_scenes() 273 | if scenes is not None: 274 | for scene in scenes: 275 | name = obs.obs_source_get_name(scene) 276 | obs.obs_property_list_add_string(p_scene, name, name) 277 | 278 | obs.source_list_release(scenes) 279 | 280 | sources = obs.obs_enum_sources() 281 | if sources is not None: 282 | for source in sources: 283 | source_id = obs.obs_source_get_unversioned_id(source) 284 | 285 | if source_id == 'ffmpeg_source': 286 | name = obs.obs_source_get_name(source) 287 | obs.obs_property_list_add_string(p_source, name, name) 288 | 289 | obs.source_list_release(sources) 290 | 291 | return props 292 | --------------------------------------------------------------------------------