From 2c6a3452192aeeba5d9abb02ba9e3f070a208a1e Mon Sep 17 00:00:00 2001 From: "Claude (chzzk-bypass owner)" Date: Thu, 28 May 2026 01:27:58 +0900 Subject: [PATCH] =?UTF-8?q?=ED=83=80=EC=9E=84=EB=A8=B8=EC=8B=A0=20?= =?UTF-8?q?=EA=B0=95=EC=A0=9C=20=ED=99=9C=EC=84=B1=ED=99=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20README=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - timemachine.js: live-detail 응답을 fetch 후킹으로 가로채 timeMachineActive 를 true 로 만들고, live-playback-json 의 DVR 매니페스트로 livePlaybackJson 을 교체해 되감기 UI 와 실제 seek 동작을 살린다. 스트리머가 타임머신을 꺼둔 라이브에서도 동작. - manifest.json: timemachine.js 를 content_scripts 에 추가하고 버전을 1.1.0 으로 올림. name/description 을 다기능에 맞게 갱신. - popup.html: 타임머신 강제 활성화 상태 표시 항목 추가. - README.md: 기능 설명, 설치 방법, 동작 확인 로그, 면책 명시. --- README.md | 70 ++++++++++++++++++++++++ manifest.json | 9 ++-- popup.html | 10 ++-- timemachine.js | 143 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 README.md create mode 100644 timemachine.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b1b1d6 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Chzzk Bypass + +치지직(CHZZK) 시청 환경을 개선하는 Chrome / Edge / Whale 용 확장 프로그램 (Manifest V3). + +## 기능 + +### 1. 그리드 우회 (1080p 시청) + +`navigator.userAgent`, `navigator.platform`, `navigator.userAgentData` 를 Mac 으로 +위장해, 치지직이 윈도우에서 1080p 시청 시 요구하는 그리드(Grid) 설치 없이 1080p +화질을 시청할 수 있게 합니다. + +구현: `content.js` + +### 2. 타임머신 강제 활성화 (되감기 UI) + +스트리머가 타임머신을 끄고 방송 중이어도 플레이어에 되감기 (seek) 바 UI 를 +띄웁니다. 가능하면 DVR 가능한 HLS 매니페스트까지 함께 갈아끼워 실제 되감기 +동작도 살립니다. + +동작 방식: + +1. `api.chzzk.naver.com/service/vN/channels/{id}/live-detail` 응답을 `fetch` + 후킹으로 가로챕니다. +2. `content.timeMachineActive` 플래그를 강제로 `true` 로 만듭니다 → 플레이어 + UI 가 되감기 바를 노출합니다. +3. 같은 채널의 `…/channels/{id}/live-playback-json` 을 호출해 DVR 매니페스트 + 문자열을 받아 `content.livePlaybackJson` 을 교체합니다 → 매니페스트가 실제 + seek 을 지원하게 됩니다. + +이는 [Choonholic/ChzzkDownloader](https://github.com/Choonholic/ChzzkDownloader) +의 `--stream force-timemachine` 옵션이 사용하는 것과 같은 엔드포인트입니다. +관련 글: [blog.choonholic.com/archives/3216](https://blog.choonholic.com/archives/3216) + +구현: `timemachine.js` + +> 주의: 타임머신 매니페스트가 보유한 되감기 가능 길이는 CDN 측 윈도우에 +> 의존하므로, 강제로 활성화했다 하더라도 무한정 과거로 되감을 수 있는 것은 +> 아닙니다. + +## 설치 + +1. 이 저장소를 `git clone` 또는 ZIP 다운로드해 임의의 폴더에 둡니다. +2. Chrome 계열 브라우저에서 `chrome://extensions` 접속. +3. 우상단 **개발자 모드** 활성화. +4. **압축해제된 확장 프로그램을 로드합니다** 클릭 → 위 폴더 선택. +5. 치지직(`https://chzzk.naver.com`) 새로고침. + +## 파일 구조 + +``` +manifest.json 확장 정의 (MV3) +content.js 그리드 우회 (UA / platform / UA-CH 위장) +timemachine.js 타임머신 강제 활성화 (fetch 후킹) +popup.html/css/js 툴바 팝업 +icons/ 확장 아이콘 +``` + +## 동작 확인 + +브라우저 devtools 콘솔에서 다음 로그를 확인할 수 있습니다. + +- `[chzzk-bypass:timemachine] fetch hook installed` — 후킹 성공 +- `[chzzk-bypass:timemachine] forcing timeMachine ON for ` — 패치 적용 +- `[chzzk-bypass:timemachine] livePlaybackJson swapped for DVR manifest` — 매니페스트 교체 성공 + +## 면책 + +본 확장은 학습 및 개인 사용 목적의 우회 도구입니다. 사용에 따른 모든 책임은 +사용자에게 있으며, 치지직 약관 및 관련 법령을 준수하여 사용하시기 바랍니다. diff --git a/manifest.json b/manifest.json index 9f6ae9e..f67dfa1 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { - "name": "Chzzk Mac Spoofer (Grid Bypass)", - "version": "1.0.0", + "name": "Chzzk Bypass", + "version": "1.1.0", "manifest_version": 3, - "description": "치지직(CHZZK) 시청 시 브라우저 환경을 Mac으로 위장하여 그리드 설치 없이 1080p 화질을 쾌적하게 시청합니다.", + "description": "치지직(CHZZK) 시청 환경 개선: 1) Mac 위장으로 그리드 없이 1080p 시청, 2) 스트리머가 타임머신을 꺼둔 라이브에서도 되감기 UI 강제 표시.", "icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", @@ -35,7 +35,8 @@ "*://chzzk.naver.com/*" ], "js": [ - "content.js" + "content.js", + "timemachine.js" ], "run_at": "document_start", "world": "MAIN" diff --git a/popup.html b/popup.html index 09ec379..1a5678f 100644 --- a/popup.html +++ b/popup.html @@ -11,15 +11,19 @@ 작동 중
-

현재 Mac OS로 위장하여
그리드를 우회하고 있습니다.

+

치지직 시청 환경을 개선합니다.

대상 사이트 chzzk.naver.com
- 우회 상태 - 정상 (1080p) + 그리드 우회 + Mac 위장 (1080p) +
+
+ 타임머신 + 강제 활성화
diff --git a/timemachine.js b/timemachine.js new file mode 100644 index 0000000..13f7fa0 --- /dev/null +++ b/timemachine.js @@ -0,0 +1,143 @@ +// 치지직 라이브 응답을 가로채 타임머신 기능을 강제로 활성화한다. +// 스트리머가 타임머신을 꺼둔 방송에서도 플레이어에 되감기(seek) UI 를 표시하고, +// 가능하면 DVR 매니페스트(URL) 까지 함께 갈아끼워 실제 되감기도 동작하도록 만든다. +// +// 동작 원리 +// - 치지직 웹 플레이어는 `api.chzzk.naver.com/service/vN/channels/{id}/live-detail` +// 응답의 `content.timeMachineActive` 플래그로 되감기 UI 노출 여부를 결정한다. +// - 같은 채널의 `live-playback-json` 엔드포인트는 DVR 가능한 HLS 매니페스트 URL 을 +// 항상 돌려준다. ChzzkDownloader 의 `--stream force-timemachine` 옵션이 쓰는 +// 바로 그 엔드포인트다. +// - 따라서 `live-detail` 응답을 가로채서 (1) 플래그를 강제로 true 로 만들고, +// (2) `livePlaybackJson` 안의 매니페스트 URL 을 `live-playback-json` 응답으로 +// 교체하면, 스트리머 설정과 무관하게 되감기 UI 와 실제 seek 동작 모두 살아난다. +// +// manifest 의 `world: "MAIN"`, `run_at: "document_start"` 덕분에 페이지 자체 스크립트가 +// 실행되기 전에 fetch 를 후킹할 수 있다. + +(function () { + 'use strict'; + + const TAG = '[chzzk-bypass:timemachine]'; + const LIVE_DETAIL_RE = /^https:\/\/api\.chzzk\.naver\.com\/service\/v\d+\/channels\/([^\/?#]+)\/live-detail/; + + // 일부 빌드에서는 v3, 어떤 시점에는 v2/v1 만 응답하므로 순차 시도한다. + const PLAYBACK_JSON_VERSIONS = ['v3', 'v2', 'v1']; + + function log() { + try { + // eslint-disable-next-line no-console + console.log.apply(console, [TAG].concat(Array.prototype.slice.call(arguments))); + } catch (_) {} + } + + const originalFetch = window.fetch.bind(window); + + async function fetchPlaybackJson(channelId) { + for (const ver of PLAYBACK_JSON_VERSIONS) { + try { + const url = `https://api.chzzk.naver.com/service/${ver}/channels/${channelId}/live-playback-json`; + const resp = await originalFetch(url, { credentials: 'include' }); + if (!resp.ok) continue; + const json = await resp.json(); + const playback = json && json.content && json.content.playbackJson; + if (playback) { + log('playback-json hit via', ver); + return playback; + } + } catch (e) { + log('playback-json fetch error on', ver, e); + } + } + return null; + } + + // livePlaybackJson 은 외부에선 JSON 문자열로 들어오지만, 가끔 이미 파싱된 + // 객체 형태로 들어오는 빌드도 있다. 두 경우 모두 처리한다. + function setLivePlaybackJson(content, replacement) { + if (typeof content.livePlaybackJson === 'string') { + content.livePlaybackJson = typeof replacement === 'string' + ? replacement + : JSON.stringify(replacement); + } else { + content.livePlaybackJson = typeof replacement === 'string' + ? JSON.parse(replacement) + : replacement; + } + } + + async function patchLiveDetail(response, channelIdFromUrl) { + let data; + try { + data = await response.clone().json(); + } catch (e) { + log('response is not JSON, passthrough'); + return response; + } + + const content = data && data.content; + if (!content) return response; + + const alreadyOn = content.timeMachineActive === true || content.timeMachine === true; + const channelId = (content.channel && content.channel.channelId) || channelIdFromUrl; + + if (alreadyOn) { + log('timeMachine already active for', channelId, '— passthrough'); + return response; + } + + log('forcing timeMachine ON for', channelId); + content.timeMachineActive = true; + content.timeMachine = true; + + // DVR 매니페스트로 교체 시도. 실패해도 플래그는 살려서 UI 만이라도 노출. + if (channelId) { + const playback = await fetchPlaybackJson(channelId); + if (playback) { + setLivePlaybackJson(content, playback); + log('livePlaybackJson swapped for DVR manifest'); + } else { + log('playback-json unavailable; UI shown but seek may not work'); + } + } + + const newBody = JSON.stringify(data); + const headers = new Headers(response.headers); + headers.set('content-length', String(new Blob([newBody]).size)); + return new Response(newBody, { + status: response.status, + statusText: response.statusText, + headers, + }); + } + + window.fetch = async function patchedFetch(input, init) { + let url = ''; + try { + url = typeof input === 'string' + ? input + : (input && input.url) || ''; + } catch (_) { url = ''; } + + const match = url && url.match(LIVE_DETAIL_RE); + if (!match) { + return originalFetch(input, init); + } + + let response; + try { + response = await originalFetch(input, init); + } catch (e) { + throw e; + } + + try { + return await patchLiveDetail(response, match[1]); + } catch (e) { + log('patch failed, returning original response', e); + return response; + } + }; + + log('fetch hook installed'); +})();