대표 지적 여섯을 한 번에 고친다. - **재발행이 Suno 를 다시 부르던 것**(`song_service.ensure_song`). `build_service` 가 `publish=True` 마다 이 함수를 불렀는데 가드가 없었다 — 내용이 하나도 안 바뀐 재발행에도 Gemini 가사 1회 + Suno 작곡 1회가 그대로 나갔고, 다섯 번 누르면 유료 호출 다섯 번에 `place_songs` 행 다섯 개다. 화면은 최신 READY 한 곡만 쓰므로 나머지는 돈만 쓴다. → READY 곡이 있으면 건너뛴다. 일부러 다시 만드는 길은 SONG 잡(`force=True`)으로 남긴다 - **엽서 쓰기 자리**를 계절별 축제 바로 앞으로(시연본과 같게). 목록 맨 뒤라 FAQ 보다도 아래였다. 축제가 없는 사이트는 종전대로 맨 뒤 - **엽서가 모바일에서 화면을 밀어내던 것** — 그리드 칸의 기본 `min-width:auto` 가 사진 레일의 최소 너비(실측 390px 화면에서 2,056px)를 그대로 받아 칸이 밖으로 나갔다. `min-w-0` 과 캔버스 비율 고정(`aspect-square h-auto`). 실측 넘침 0px · 캔버스 358×358 - **가요 다방 판이 한 색이던 것** — 폴백 팔레트를 시연본(`/s/stay`)이 쓰는 23색 그대로 옮겼다(대표: "그냥 정해놔 · /s/stay 보고 맞춰라"). 목록을 새로 짜면 시연본과 갈린다 - **푸터에 만든 곳 표기** — `AI O2O의 Web4Ai로 만든 사이트입니다.`(o2osolution.ai 링크). 발행본(`SiteFooter`)과 시연본(`patch_stay.py`) 둘 다 - **에디터 캔버스의 푸터·모바일 탭바 제거** — 발행본이 iframe 안에서 둘 다 이미 그린다 (`SiteFooter` · `MobileTabBar`). 바깥에 한 벌 더 그려 두 번 서 있었다 - **시연본 자동재생 끔**(`inject.js`, 대표: "클릭하고 나서 되니까 사람들이 에러로 보잖아"). 세 번째 뒤집기고 이번 이유는 다르다 — 정책을 못 이겨서가 아니라 정책에 걸린 모습이 고장으로 읽혀서다. 함수는 남기고 호출만 뺐다. 제품 렌더러엔 원래 없다 검증: tsc·eslint 통과(site·frontend), site 79 passed. 실제로 구워서 확인 — 섹션 순서 `…오시는 길 → 엽서쓰기 → 계절별 축제 → …`, 푸터에 o2osolution.ai 링크, 모바일 가로 넘침 0. 킹서버 실측: 재발행 안 한 사이트(joyyy·bbbb)도 중계로 공유 버튼이 뜬다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
152 lines
7.9 KiB
Markdown
152 lines
7.9 KiB
Markdown
# 자동재생 — 안 된다. 왜 안 되는지와, 그래서 뭘 했는지
|
|
|
|
> ★ **2026-09-15 — 자동재생을 껐다.** 대표: "노래가 자동재생 안 되고 클릭하고 나서 되니까
|
|
> 사람들이 에러로 보잖아 그냥 꺼놔." 아래 내용은 **왜 자동재생이 어려운가**의 기록으로 남긴다
|
|
> — 되살릴 일이 생기면 `inject.js` 의 `void autoplay;` 를 `autoplay();` 로 되돌리면 된다.
|
|
> 함수와 첫 손길 처리는 지우지 않았다.
|
|
|
|
`/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.
|