o2o-site-AEO/solution/site/scripts/mockup/AUTOPLAY.md
Mina Choi 5e2def200b [chore] site/mockup: 번들 갱신·문서 정리 · 2안(사진) 빌더 스크립트 추가
- vendor/ 옛 해시 번들을 retired/ 로 옮기고 새 해시로 교체
- build_photo6.py 신설 — /s/stay6 "2안(사진)" 판, 슬러그별로 다시 구울 수 있다
  (python3 build_photo6.py <slug>)
- build_pension.py·build_reading.py·build_stay6.py·patch_stay.py 정리
- AUTOPLAY.md·README.md·audit-all.mjs 갱신
2026-09-23 13:18:51 +09:00

153 lines
8.0 KiB
Markdown

# 자동재생 — 안 된다. 왜 안 되는지와, 그래서 뭘 했는지
> ★ **2026-09-22 — 자동재생은 켠다. 끄지 않는다.** 대표 지시("내가 키라고 몇번을 말해").
> `inject.js:600` 은 `autoplay();` 다. `void autoplay;` 로 되돌리는 변경은 하지 않는다.
> (09-15 에 "에러로 보이니 꺼놔" 로 한 번 껐던 것을 되돌린 것이다 — 09-11 빼 → 09-14 켜 →
> 09-15 끔 → 09-22 켜.) 아래는 **왜 첫 방문에 소리가 안 나는가**의 기록이지,
> 호출을 빼라는 근거가 아니다 — 브라우저가 거절해도 호출은 건다.
`/s/stay` 배경음악을 **새로고침 직후 바로** 나게 하려고 시도한 것 전부와 실측값이다.
같은 얘기가 다시 나오면 이 파일부터 본다.
측정: 2026-09-14 · 킹서버 `https://web4ai.o2osolution.ai/s/stay` · Chrome(정식 채널)
---
## 결론
**브라우저에서 첫 방문에 소리가 저절로 나게 하는 방법은 없다.**
사람 손 입력(`isTrusted: true`) 없이는 크롬이 `play()``NotAllowedError` 로 거절한다.
자바스크립트로 그 검사를 통과시키는 길은 없고, 그건 우회 대상이 아니라
**그 우회를 막으려고 만들어진 검사**다.
지금 동작: **손님이 화면에 처음 닿으면(클릭·탭·키) 켜진다.** 그 이상은 어떤 사이트도 못 한다.
### 해 본 것과 결과
| 시도 | 결과 |
|---|---|
| 로드 즉시 `audio.play()` | `NotAllowedError` |
| 무음(`muted=true`)으로 시작 → 나중에 unmute | `NotAllowedError` (무음도 거절된다) |
| `<audio muted playsinline>` 태그로 두고 `play()` | `NotAllowedError` |
| 가짜 이벤트 강제 발생 — `dispatchEvent``pointerdown`·`mousedown`·`pointerup`·`mouseup`·`click`·`keydown` 6종 | `navigator.userActivation` **false 그대로**, 거절 |
| 재생 단추를 코드로 누르기 — `btn.click()`, 0.7초 간격 3회 | `play=0` |
| `--app=URL` 앱 모드로 띄우기 | `play=0` |
| **Web Audio API**`new AudioContext()` | `state: "suspended"` |
| `ctx.resume()` | **약속이 영원히 안 돌아온다** (제스처를 기다리며 매달린다) |
| mp3 를 `decodeAudioData` 로 풀어 `BufferSource` 재생 | `currentTime` **0.000 → 0.000**, 안 돈다 |
| **`speechSynthesis.speak()`** | `error: not-allowed` |
| 마우스만 움직임 / 스크롤만 | `play=0` (`act=false`) |
| **진짜 클릭·탭·키 입력 한 번** | **켜진다** (`act=true`, `play=1`) |
`dispatchEvent``el.click()` 이 만든 이벤트는 `isTrusted: false` 로 찍혀 나간다.
크롬의 사용자 활성화 카운터(`navigator.userActivation`)는 `isTrusted: true` 인 입력만 세고,
그게 올라야 소리가 풀린다.
브라우저에서 **소리를 낼 수 있는 API 는 셋뿐이고 셋 다 같은 게이트 뒤에 있다**
HTMLMediaElement(`<audio>`) · Web Audio(`AudioContext`) · speechSynthesis. 우회로가 없는 이유다.
**활성화를 준다**: `mousedown` · `pointerdown` · `pointerup` · `touchend` · `keydown`(Esc 제외) · `click`
**안 준다**: `mousemove` · `pointermove` · `touchmove` · `wheel` · `scroll`
---
## 안 한 것과 그 이유
크롬 문서(developer.chrome.com/blog/autoplay)에 예외가 넷 있지만 **전부 손님 쪽 설정이라
제품에서는 쓸 수 없다.** 한 번 만들었다가 되돌렸다.
| 예외 | 왜 안 쓰나 |
|---|---|
| PWA 설치본 | **펜션 사이트 보러 온 손님이 앱을 설치할 리 없다.** 매니페스트까지 붙였다가 걷어냈다 |
| MEI (같은 출처에서 7초 이상 여러 번) | 손님이 쌓는 것이라 첫 방문에는 없다 |
| `--autoplay-policy=no-user-gesture-required` | 브라우저를 그 플래그로 띄워야 한다 |
| 기업정책 `AutoplayAllowlist` | 기계 관리자가 넣는 것이다 |
**강제 클릭 코드도 뺐다.** 넣어서 배포까지 해 봤지만 기본 크롬에서 `play=0` 이고,
동작하지 않는 코드가 남으면 다음 사람이 "이미 처리돼 있네" 로 읽는다.
---
## 지금 코드가 하는 일 (`inject.js` `startPlayer`)
1. 로드 즉시 `play()` 를 건다. (거절되지만, 자동재생이 허락된 환경이면 여기서 켜진다)
2. 거절되면 **첫 사람 손길**을 기다린다. 창 전체에 걸고, 플레이어 자기 단추만 빼고
어디를 눌러도 걸린다.
- STRONG(활성화를 주는 입력): 스로틀 없이 매번 시도
- WEAK(안 주는 입력): 1초에 한 번만 — 초당 수십 번 오는 mousemove 로 단추가 깜빡이는 것을 막는다
3. 탭이 다시 앞으로 나올 때(`visibilitychange`) 한 번 더 건다.
### 여기서 밟았던 함정 둘
- **가짜 pointerdown 이 대기를 태웠다.** `tameRails()` 가 레일 자동 넘김을 세우려고 레일마다
`pointerdown` 을 쏘는데(정상 동작), 그게 첫 손길 대기를 먼저 먹었다. 그 뒤로는 손님이
아무 데나 눌러도 안 켜졌다. → `onFirstTouch``event.isTrusted` 를 먼저 본다.
- **스로틀이 클릭을 걸렀다.** 입력을 안 가리고 1초 스로틀을 걸었더니 클릭 직전의 mousemove 가
그 1초를 먹어 **정작 활성화를 주는 mousedown 이 걸러졌다**(실측: `act=true` 인데 `play=0`).
→ STRONG/WEAK 로 갈라 STRONG 에는 스로틀을 안 건다.
---
## "트리플픽은 되던데?" — 실측 결과
**트리플픽도 안 된다. 똑같이 거절당한다.**
`https://triplepick.o2o.kr/match/MLB_BAL_TOR_20260914?song=BAL` (공유 링크 = 자동 로드 경로)에
깨끗한 크롬으로 들어가 `HTMLMediaElement.prototype.play` 를 가로채 찍은 값이다.
```
[T] play() 거절 NotAllowedError .kr/api/songs/audio/815/0?v=89a0fcd6
```
코드도 우리와 같은 한 줄이다 — `usePlayer.tsx:133` `if (shouldPlay) audio.play().catch(() => {})`.
게스처 감지 코드는 0줄이고, 평소 재생 경로는 `MatchupHUD.tsx:55`**「♪ 응원가 듣기」 버튼**이다.
`playlist.ts:19``PATH_PLAYLISTS` 는 비어 있어서 경로 진입 자동 로드는 실을 곡도 없다.
### 그럼 왜 내 브라우저에서는 나나 — MEI
크롬은 **같은 출처에서 소리를 여러 번 들은 사람에게만** 그 출처의 자동재생을 풀어 준다
(Media Engagement Index). 조건: 7초 초과 재생 · 음소거 아님 · 탭 활성 · (영상이면) 200x140px 초과.
트리플픽을 자주 쓰면서 응원가를 여러 번 들었으면 그 브라우저에는 이미 허락이 쌓여 있다.
**그 브라우저에서만** 자동재생된다. 손님의 새 브라우저에서는 안 된다.
확인: `chrome://media-engagement/` 에서 출처별 점수와 `Has high score` 를 본다.
**같은 일이 이 사이트에도 일어난다.** 목업을 자주 열고 음악을 들으면 그 브라우저는
`web4ai.o2osolution.ai` 에 허락을 쌓고, 그 뒤로는 새로고침해도 바로 난다.
**대표님 노트북에서 시연하면 그 상태가 된다** — 다만 손님 브라우저는 아니다.
---
## 재보고 싶을 때 — 측정 주의
**Playwright 로 잴 때 `page.evaluate` 를 쓰면 안 된다.** CDP 가 `userGesture: true` 로 넣어서
**상태를 읽으려고 부른 그 호출이 브라우저를 "조작됨" 으로 만든다.** 그 뒤 측정은 전부 거짓
양성이다 — 실제로 이것 때문에 "마우스만 움직여도 켜진다" 는 틀린 결론을 한 번 냈다.
상태는 `addInitScript` 로 심고 `console` 로만 받는다.
```js
await page.addInitScript(() => {
setInterval(() => {
const el = document.getElementById('w4d-play');
console.log('[C] play=' + (el ? el.dataset.playing : '-')
+ ' act=' + navigator.userActivation.isActive);
}, 1000);
});
page.on('console', (m) => { /* 여기서만 읽는다 */ });
```
---
## 지금 무엇을 보고 있는지 가리는 법
`/s/``max-age=300, must-revalidate` 다. "고쳤는데 안 된다" 의 상당수가 **캐시된 옛 파일**이었다.
콘솔 첫 줄에 빌드 시각이 찍힌다.
```
[w4d] 주입분 로드 · 빌드 2026-09-14T07:12:00+00:00
```
이 시각이 방금 구운 것과 다르면 옛 파일을 보고 있는 것이다. ⌘⇧R.