타임머신 강제 활성화 기능 추가 및 README 작성
- 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: 기능 설명, 설치 방법, 동작 확인 로그, 면책 명시.
This commit is contained in:
70
README.md
Normal file
70
README.md
Normal file
@@ -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 <channelId>` — 패치 적용
|
||||||
|
- `[chzzk-bypass:timemachine] livePlaybackJson swapped for DVR manifest` — 매니페스트 교체 성공
|
||||||
|
|
||||||
|
## 면책
|
||||||
|
|
||||||
|
본 확장은 학습 및 개인 사용 목적의 우회 도구입니다. 사용에 따른 모든 책임은
|
||||||
|
사용자에게 있으며, 치지직 약관 및 관련 법령을 준수하여 사용하시기 바랍니다.
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "Chzzk Mac Spoofer (Grid Bypass)",
|
"name": "Chzzk Bypass",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"description": "치지직(CHZZK) 시청 시 브라우저 환경을 Mac으로 위장하여 그리드 설치 없이 1080p 화질을 쾌적하게 시청합니다.",
|
"description": "치지직(CHZZK) 시청 환경 개선: 1) Mac 위장으로 그리드 없이 1080p 시청, 2) 스트리머가 타임머신을 꺼둔 라이브에서도 되감기 UI 강제 표시.",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/icon16.png",
|
"16": "icons/icon16.png",
|
||||||
"48": "icons/icon48.png",
|
"48": "icons/icon48.png",
|
||||||
@@ -35,7 +35,8 @@
|
|||||||
"*://chzzk.naver.com/*"
|
"*://chzzk.naver.com/*"
|
||||||
],
|
],
|
||||||
"js": [
|
"js": [
|
||||||
"content.js"
|
"content.js",
|
||||||
|
"timemachine.js"
|
||||||
],
|
],
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"world": "MAIN"
|
"world": "MAIN"
|
||||||
|
|||||||
10
popup.html
10
popup.html
@@ -11,15 +11,19 @@
|
|||||||
<span class="badge active">작동 중</span>
|
<span class="badge active">작동 중</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>현재 <strong>Mac OS</strong>로 위장하여<br>그리드를 우회하고 있습니다.</p>
|
<p>치지직 시청 환경을 개선합니다.</p>
|
||||||
<div class="status-box">
|
<div class="status-box">
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<span class="label">대상 사이트</span>
|
<span class="label">대상 사이트</span>
|
||||||
<span class="value">chzzk.naver.com</span>
|
<span class="value">chzzk.naver.com</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<span class="label">우회 상태</span>
|
<span class="label">그리드 우회</span>
|
||||||
<span class="value success">정상 (1080p)</span>
|
<span class="value success">Mac 위장 (1080p)</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="label">타임머신</span>
|
||||||
|
<span class="value success">강제 활성화</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
143
timemachine.js
Normal file
143
timemachine.js
Normal file
@@ -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');
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user