<video> 태그가 아니라 WebSocket으로 "문자 데이터"만 흘려보내 브라우저 Canvas에 그려 주는 실시간 영상 엔진이다. 핵심 주장은 도발적이다 — "웹은 원래 텍스트로 만들어졌으니, 영상도 텍스트로 보내자." 그 결과 영상에 CSS 필터(네온 글로우·그림자)를 입히고, 광고 차단기·자동재생 제한을 우회하며, 초저대역폭으로 IoT까지 스트리밍할 수 있는 "타이포그래피 캔버스"가 된다. (저장소: YusufB5/ASCILINE · Python(FastAPI)+Vanilla JS · MIT 라이선스 + 광고 금지 조항)stream_server.py·app.js·codec.py·codec.js 소스를 직접 뜯어, "실시간 영상-텍스트 변환 파이프라인과 적응형 코덱에서 무엇을 배울 수 있는가"에 초점을 둔 학습 해설이다.이 레포가 무엇을 하는 물건인가.
보통 웹 영상은 <video> 태그가 H.264/VP9 같은 코덱을 GPU로 풀어 화면에 뿌린다. ASCILINE은 이 흐름을 통째로 바꾼다. 서버(Python)가 영상을 한 번 디코딩해 각 픽셀을 밝기에 맞는 ASCII 문자 + RGB 색으로 매핑하고, 그 결과를 바이너리 프레임으로 WebSocket에 실어 보낸다. 브라우저는 받은 문자 데이터를 Canvas에 한 글자씩 그려서 영상처럼 보이게 한다.
핵심은 "재미있는 ASCII 아트"가 아니라 "영상의 실체가 텍스트/Canvas다"라는 점이다. 그래서 영상에 CSS 필터를 입히고, 글자를 드래그로 선택하고, 브라우저의 자동재생·광고 차단 정책을 우회할 수 있다 — 브라우저 입장에선 그냥 "JS가 Canvas를 갱신하는 것"이기 때문이다.
ASCILINE은 실시간 비디오-ASCII 렌더링 엔진이다. 백엔드는 FastAPI + OpenCV + NumPy로 영상을 그리드(grid)로 다운샘플해 문자/색 버퍼를 만들고, 프론트엔드는 순수 Vanilla JS가 WebSocket으로 받은 프레임을 지터 버퍼(jitter buffer)에 쌓아 requestAnimationFrame 루프로 그린다. 오디오 트랙을 마스터 클럭(master clock)으로 삼아 A/V 동기를 맞추고, 정적/저움직임 프레임에는 적응형 코덱(adaptive codec)으로 변한 셀만 보내 대역폭을 수백 배 아낀다. 웹 UI 외에 터미널에서 트루컬러 ANSI로 직접 재생하는 모드(ascii_video_player2.py)도 함께 들어 있다.
" .:-=+*#%@" 밝음)에 매핑해 글자로 그림을 그리는 기법. ASCILINE은 여기에 픽셀의 RGB 색을 함께 실어 "컬러 ASCII 영상"을 만든다. 한 글자가 영상 한 셀(cell)에 대응한다.frameBuffer(크기 약 4프레임)가 이 역할을 한다 — 실시간 스트리밍 영상·음성의 필수 부품."영상=텍스트"라는 발상이 푸는 네 가지 실전 문제.
단순히 "옛날 ASCII 무비 플레이어"였다면 트렌딩하지 못했을 것이다. ASCILINE이 주목받는 이유는 "영상의 실체를 텍스트로 바꾸면 생기는 부수효과"를 전면에 내세웠기 때문이다. README가 정리한 네 가지 "전략적 비전"은 그대로 차별점이 된다.
| 축 | 일반 웹 영상(<video> / HLS) | ASCILINE |
|---|---|---|
| 실체 | 압축 바이너리(GPU 디코딩) | HTML/Canvas 위의 순수 문자 |
| 스타일링 | 영상 위 오버레이만 가능 | 영상 자체에 CSS 필터·글로우·애니메이션 |
| 대역폭 | 코덱이 고정(약함=버벅임) | 프레임당 수 KB, 적응형 코덱으로 가변 |
| 브라우저 제약 | 자동재생 throttle·광고차단 표적 | "JS가 canvas 갱신" — 제약 우회 |
| AI 친화성 | 무거운 CV 모델 필요 | 구조화된 문자열 → 경량 LLM이 의미 요약 |
| IoT/약한 기기 | 마이크로컨트롤러가 코덱 못 풂 | 서버가 무거운 일 처리, 몇 KB만 전송 |
일반 웹 영상은 GPU가 디코딩하는 불투명한 픽셀 덩어리다. 그래서 ① 영상 콘텐츠에 텍스트처럼 CSS 필터를 직접 못 입히고, ② 약한 네트워크/마이크로컨트롤러는 H.264를 풀 여력이 없으며, ③ 브라우저는 자동재생 영상을 적극적으로 throttle하고 광고차단기가 미디어 프레임을 막는다. "영상=무거운 바이너리"라는 전제 자체가 병목이다.
ASCILINE은 픽셀→문자 변환이라는 무거운 연산을 백엔드에서 한 번만 수행한다. 클라이언트로는 그리드 크기만큼의 문자+색 버퍼(또는 적응형 압축본)만 흐른다. 정적 화면에선 변한 셀만 전송(DELTA)해 레거시 대비 0.3%(약 375배 절감)까지 줄어든다. 게다가 출력이 "텍스트/Canvas"이므로 CSS·DOM의 모든 무기를 그대로 쓸 수 있다 — 이것이 "타이포그래피 캔버스"라는 비전의 실체다.
일반 영상 스트리밍이 "완성된 그림을 통째로 택배로 보내는 것"이라면, ASCILINE은 "색칠 도안의 '바뀐 칸 좌표와 색만' 우편엽서로 보내는 것"에 가깝다. 화면 대부분이 그대로면(슬라이드·정지화면) 보낼 게 거의 없고, 받는 쪽은 도안 위 그 칸만 다시 칠하면 된다. 그래서 정적 콘텐츠에서 대역폭이 극적으로 줄어든다.
백엔드(변환 엔진)·프론트엔드(렌더러)·코덱/인프라 각각.
백엔드의 일은 "영상 프레임을 그리드로 줄여 문자/색 버퍼로 만들고, WebSocket으로 밀어내는 것"이다. CPU 연산(리사이즈·인덱싱)이 핵심이라 NumPy 벡터화가 성능의 열쇠다.
| 요소 | 역할 |
|---|---|
| Python 3.10+ | 전체 백엔드 언어. 타입힌트·async/await 사용. |
FastAPI + uvicorn | HTTP 라우트(/, /static, /audio)와 @app.websocket("/ws") 엔드포인트. |
OpenCV (cv2) | 영상 디코딩·프레임 추출·그리드 리사이즈. cap.grab()로 디코딩 없이 프레임 스킵(FPS 데시메이션). |
NumPy | 밝기→문자 인덱스 매핑(floor_divide+LUT), 컬러 양자화((rgb>>qb)<<qb), 델타 비교를 모두 벡터 연산으로. |
websockets | send_bytes/send_text로 프레임 전송, ConnectionClosed 처리. |
| FFmpeg (subprocess) | /audio 스트림 생성. --vol 배율을 volume 필터로 적용. --vol 0이면 아예 실행 안 함. |
ascii_video_player2.py | 엔진 공용 모듈. VideoDecoder·AsciiMapper를 서버와 터미널 플레이어가 공유. |
프레임워크가 전혀 없다(React·Vue 무관). app.js 한 파일이 WebSocket 수신·지터 버퍼·A/V 동기·Canvas 그리기를 다 한다. "장식 애니메이션 0, 순수 성능"이 코드 주석의 선언이다.
| 요소 | 역할 |
|---|---|
| WebSocket (binary) | binaryType='arraybuffer'. /ws?codec=adaptive로 접속해 적응형 프레임 수신. |
| Canvas 2D | 컬러 모드(2~5)는 fillText로 글자별 색 그리기. 픽셀 모드는 ImageData 제로카피 픽셀 채우기. |
requestAnimationFrame | 렌더 루프. 마스터 클럭과 비교해 늦은 프레임은 버리고(catch-up), 미래 프레임은 대기. |
<pre> 선택 레이어 | Canvas 위에 투명 <pre>를 겹쳐 "보이지 않는 텍스트"를 깔아, 영상 글자를 마우스로 드래그 선택·복사하게 함. |
<audio> 마스터 클럭 | 오디오 currentTime이 절대 기준 시계. 오디오가 없으면 performance.now() 폴백. |
| RLE 색 캐싱 | 이전 셀과 색이 같으면 fillStyle을 다시 안 바꿔(prevPacked) 드로우 오버헤드 절감. |
| 요소 | 역할 |
|---|---|
적응형 코덱(codec.py/codec.js) | 프레임마다 RAW/ZLIB/DELTA 중 가장 작은 인코딩 선택, 1바이트 태그로 표시. Python 인코더 ↔ JS 디코더가 바이트 단위로 동일. |
| 키프레임 리싱크 | KEYFRAME_INTERVAL=48마다 전체 프레임 강제 전송 — 패킷 유실·늦게 접속한 클라이언트 재동기. |
| 손실 시간 델타(선택) | --quality로 색 셀이 허용 오차(tolerance)를 넘을 때만 재전송. 문자 평면은 항상 정확, 기본은 무손실. |
| 정적 파일 화이트리스트 | STATIC_WHITELIST = {app.js, style.css, codec.js} — 디렉토리 트래버설 차단. |
| Origin 검증 | _origin_allowed() — localhost·동일 출처만 WS 허용해 cross-site WebSocket hijacking 방지(LAN 모드 호환). |
| 크로스 검증 테스트 | experiments/: Python 인코딩 벡터를 Node가 디코딩해 byte-exact 확인 + 라이브 adaptive↔legacy diff. |
| 배포/실행 | CLI 직접 실행(python stream_server.py). --folder/--playlist로 큐 관리, --loop 무한 반복. |
ImageData.data에 직접 R↔B만 스왑해 채운다(for(src,dst) 루프). 복사가 사라져 고해상도에서 CPU를 아낀다.영상 → 그리드 → 프레임 인코딩 → WS → Canvas, 그리고 A/V 동기.
입력은 MP4 한 개(또는 폴더/플레이리스트). 서버는 영상을 열어 폭(--cols)에 맞춰 그리드 크기(rows×cols)를 종횡비로 자동 계산하고, INIT 핸드셰이크로 FPS·모드·그리드를 클라이언트에 알린다. 이후 프레임마다 OpenCV로 읽어 NumPy로 문자/색 버퍼를 만들고, 적응형 코덱으로 인코딩해 WebSocket에 실어 보낸다. 동시에 /audio가 FFmpeg로 오디오를 스트림한다. 클라이언트는 프레임을 버퍼에 쌓고, 오디오 시계를 기준으로 정확한 타이밍에 Canvas에 그린다.
변환의 심장은 "밝기값(0~255)을 문자 팔레트 인덱스로 바꾸는" 한 줄이다. AsciiMapper는 어두움→밝음 순으로 정렬된 92글자 팔레트를 들고, 파이썬 루프 없이 NumPy 인덱싱으로 그리드 전체를 한 번에 변환한다.
# 팔레트(어두움→밝음). 길이 n=93
PALETTE = " `.-':_,^=;><+!rc*/z?s ... %&@" # 끝일수록 밝다
# gray: (rows, cols) uint8 밝기 행렬
indices = np.floor_divide(gray, max(1, 256 // n)) # 0..n-1 구간으로
np.clip(indices, 0, n - 1, out=indices)
char_codes = char_byte_lut[indices] # 인덱스 → 문자코드, 통째로
# 색: BGR → RGB 후 양자화(상위 비트만 남김 → 압축률↑)
rgb = bgr_frame[:, :, ::-1]
if qb > 0: rgb = (rgb >> qb) << qb # mode별 qb: 5→0, 4→2, 3→3, 2→5
# 셀당 4바이트 버퍼: [문자, R, G, B]
frame_buf[:, :, 0] = char_codes
frame_buf[:, :, 1:] = rgb
여기서 qb(양자화 비트)는 모드의 정체다. mode 5(16M컬러)는 qb=0(무손실), mode 2(512컬러)는 qb=5로 색을 거칠게 깎아 같은 색 셀이 많아지게 만든다 — 그래야 다음 단계 ZLIB/RLE 압축이 잘 먹힌다. "화질 모드"가 사실은 "압축 친화도 모드"인 셈이다.
원래 프로토콜은 매 프레임 그리드 전체를 다시 보냈다. codec.py의 encode_frame()은 프레임마다 바뀐 셀 비율(frac)을 재고, 세 인코딩 중 가장 작은 것을 골라 1바이트 태그로 표시한다. 똑똑한 점은 "양 극단의 명백한 패자는 시도조차 안 한다"는 것이다.
| 태그 | 인코딩 | 유리한 상황 | 레거시 대비(mode5,200×80) |
|---|---|---|---|
| 0 RAW | framebuffer 원본 | 압축 불가 프레임(안전망) | — |
| 1 ZLIB | zlib(framebuffer) | 일반 움직임 | 고움직임 63% (절대 손해 없음) |
| 2 DELTA | 바뀐 셀 인덱스+값만 zlib | 정적/저움직임 | 정지화면 0.3% (약 375×) |
_DELTA_MAX_FRAC = 0.60 # 이보다 많이 바뀌면 DELTA는 손해 → 안 만듦
_ZLIB_MIN_FRAC = 0.10 # 이보다 적게 바뀌면 전체 ZLIB는 손해
frac = changed.mean() # 바뀐 셀 비율
candidates = []
if frac < _DELTA_MAX_FRAC: # 적게 변함 → DELTA 후보
candidates.append((TAG_DELTA, zlib(idx + vals), delta_shown))
if frac >= _ZLIB_MIN_FRAC or not candidates:
candidates.append((TAG_ZLIB, zlib(raw), frame))
tag, payload, shown = min(candidates, key=lambda c: len(c[1])) # 가장 작은 것
if len(raw) < len(payload): # zlib이 오히려 부풀면 RAW로
tag, payload, shown = TAG_RAW, raw, frame
또 하나의 핵심은 문자 평면은 절대 손상하지 않는다는 규칙이다(tolerance는 색에만). 글자가 깨지면 ASCII 영상의 "구조"가 무너지므로, 손실 압축을 켜도 글자(채널 0)는 항상 정확히 보낸다. 48프레임마다 키프레임을 강제해 늦게 접속한 클라이언트·패킷 유실도 자동 복구한다.
영상과 소리가 어긋나는(desync) 문제를 ASCILINE은 "오디오를 마스터 클럭으로 삼는다"는 한 원칙으로 푼다. 비디오 프레임은 각자 time = frameIndex / fps를 갖고 버퍼에 쌓이고, 렌더 루프는 오디오의 현재 재생 위치와 비교해 프레임을 버리거나 기다린다.
masterClock = (audio && !audio.paused)
? audio.currentTime # 오디오가 절대 기준
: (now - streamStartTime) / 1000; # 폴백: 벽시계
# 너무 뒤처진 프레임은 버리고 따라잡기(catch-up)
while (buf.length > 1 && buf[0].time < masterClock - 0.1) buf.shift();
# 아직 미래의 프레임이면 이번 틱은 대기
if (buf[0].time > masterClock + 0.05) return;
const frame = buf.shift().data; # 지금 그릴 프레임
여기에 "오디오 준비 게이트"가 더해진다. 영상은 버퍼링해 두되 오디오의 playing 이벤트가 뜰 때까지 첫 렌더를 미룬다(0.5초 폴백 포함). 덕분에 시작 직후의 끊김(stutter)이 사라진다. 고FPS 소스(60fps)는 grab()으로 디코딩 없이 프레임을 건너뛰어(데시메이션) 30fps로 떨궈 CPU를 절반으로 줄인다.
| 패턴 | ASCILINE에서의 구현 |
|---|---|
| 벡터화 변환 | 밝기→문자, 색 양자화를 파이썬 루프 없이 NumPy 통째 연산으로. |
| 적응형 인코딩 | 프레임 특성(frac)에 따라 RAW/ZLIB/DELTA를 per-frame 선택. |
| 공유 디코더 | codec.js를 브라우저와 Node 테스트가 공유 → "배포 경로 = 테스트 경로". |
| 마스터 클럭 동기 | 오디오 currentTime 기준 drop/wait로 A/V 정렬. |
| 지터 버퍼 | 도착 지터를 작은 큐로 흡수해 끊김 완화. |
| 하위호환 옵트인 | ?codec=adaptive 없으면 레거시 프로토콜 그대로 — 기존 클라이언트 무영향. |
| 제로카피 렌더 | 픽셀 모드는 ImageData에 직접 채워 복사 제거. |
파일 10여 개로 끝나는 단출한 단일 폴더 — 그래서 읽기 좋다.
① index.html로 화면 구조(canvas + 투명 pre 선택레이어) 파악 → ② app.js의 connectWebSocket·renderFrame으로 수신·동기·그리기 흐름 → ③ ascii_video_player2.py의 AsciiMapper로 변환 공식 → ④ stream_server.py의 websocket_endpoint로 서버 루프 → ⑤ codec.py+codec.js 한 쌍을 나란히 놓고 인코더/디코더 대응 확인. 핵심 통찰은 ②③⑤에 거의 다 있다.
실시간 스트리밍·코덱·Canvas 렌더링에서 무엇을 배우나.
영상 한 프레임은 수만 개 픽셀이다. 이걸 파이썬 for로 돌면 실시간이 불가능하다. ASCILINE은 밝기→문자(floor_divide+LUT 인덱싱), 색 양자화((rgb>>qb)<<qb), 델타 비교(np.abs(a-b)>tol)를 모두 배열 통째 연산으로 처리한다. "파이썬 루프를 NumPy 연산으로 바꾸는" 사고법이 그대로 보이는 교본이다.
time으로 속도를 비교하라."무조건 zlib"이 정답이 아니다. 정지화면엔 델타가, 격변 프레임엔 전체 압축이 유리하다. ASCILINE은 바뀐 비율을 재서 후보를 만들고 가장 작은 걸 고른다. 게다가 "양 극단의 패자는 만들지도 않는" 가지치기로 CPU까지 아낀다. "정책을 데이터(프레임 특성)에 맡기는" 적응형 시스템 설계의 좋은 미니 사례다.
인코더(Python)와 디코더(JS)가 다른 언어면, "정말 똑같이 복원되나?"를 증명해야 한다. ASCILINE은 디코더 한 벌(codec.js)을 브라우저와 Node 테스트가 공유하고, Python이 만든 벡터를 Node가 풀어 바이트 단위 일치를 확인한다(check_vectors.js). "배포되는 코드가 곧 테스트되는 코드"라는 원칙은 그대로 베껴 쓸 가치가 있다.
영상·음성이 따로 흐르면 반드시 어긋난다. 해법은 "무엇이 절대 시계인지 먼저 정하는 것"이다. ASCILINE은 오디오를 시계로 삼고, 비디오 프레임을 그 시계에 맞춰 버리거나(catch-up) 기다린다(wait). 시작 끊김은 "오디오 준비 게이트"로, 고FPS 부하는 grab() 데시메이션으로 푼다. 영상 플레이어·게임 루프·메트로놈 앱 어디에나 옮길 수 있는 원리다.
requestAnimationFrame 루프에서 가짜 프레임 큐를 audio.currentTime(또는 가짜 시계)에 맞춰 drop/wait로 소비하는 30줄짜리 동기 데모를 만들어, 일부러 프레임을 늦게 넣어 catch-up이 작동하는지 확인하라.Canvas에 그린 글자는 마우스로 드래그 선택이 안 된다(그림이니까). ASCILINE은 Canvas 위에 투명한 <pre>를 정확히 겹쳐(object-fit 위치까지 맞춤) 같은 텍스트를 깐다. 색은 transparent지만 DOM 텍스트라 선택·복사가 된다. "그래픽 위에 접근성/상호작용 레이어를 겹치는" 영리한 트릭이다.
<pre>를 transform: translate/scale로 정렬해 마우스 드래그 선택이 되게 만들어 보라.설치는 가볍지만, --cols 값이 곧 "하드웨어 부담 다이얼"이다.
| 항목 | 요구사항 |
|---|---|
| 런타임 | Python 3.10+. pip install fastapi uvicorn opencv-python numpy websockets. |
| 오디오(선택) | FFmpeg 필수(서버측 볼륨 처리). winget/brew/apt 또는 exe를 프로젝트 폴더에 드롭. |
| 브라우저 | WebSocket + DecompressionStream('deflate')(zlib 해제) 지원 필요 — 현대 크롬/엣지/파이어폭스. |
| CPU/메모리 | 고정 요구는 없으나 --cols가 클수록 인코딩 부하 급증. 너무 높이면 서버가 프레임을 못 따라가 A/V desync. |
| 권장 cols | ASCII: 200~240 / 픽셀: 600~900(near-HD, CPU·VRAM 의존). 미지정 시 ASCII 200·픽셀 450 기본. |
| 네트워크 | 로컬(127.0.0.1:8000) 기본. --host 0.0.0.0로 LAN 노출(동일출처 Origin만 허용). |
| 영상 파일 | videos/에 직접 넣음(레포에는 미포함, .gitignore로 제외). experiments/make_test_clips.sh로 테스트 클립 생성 가능. |
ASCILINE은 MIT지만, 저자가 "광고 네트워크가 차단 불가능한 광고를 송출하는 용도"를 명시적으로 금지하는 조항을 LICENSE에 추가했다. 이 엔진이 광고차단기·브라우저 제약을 우회한다는 바로 그 특성이 악용될 수 있기 때문이다. 상업적 사용 전 LICENSE 전문을 반드시 확인할 것.
난이도별 5단계 — 클론 후 본인 영상으로 바로 시도.
짧은 MP4 하나를 --mode 1(B&W)부터 --mode 5 --pixel까지 바꿔 가며 돌려 보고, 각 모드의 화질·체감 FPS·상태바의 버퍼 수를 비교한다. --cols를 200→400→800으로 올리며 언제 A/V desync가 시작되는지 관찰하라.
style.css의 --accent-color·--bg-color를 바꿔 Matrix 그린을 본인 색으로 교체한다. 그다음 --debug 플래그로 서버를 띄워 터미널에 찍히는 RAW vs WIRE 바이트와 압축비를 정지화면·고움직임 영상에서 각각 읽어 보라.
ascii_video_player2.py의 DEFAULT_PALETTE를 다른 문자셋으로 바꾸고, mode별 qb(양자화 비트)를 조절해 화질과 압축률의 트레이드오프를 직접 만든다. 터미널 플레이어(ascii_video_player2.py)로 트루컬러 ANSI 출력 차이도 확인하라.
experiments/make_test_clips.sh로 테스트 영상을 만들고, gen_vectors.py → check_vectors.js를 돌려 Python 인코딩을 Node가 byte-exact로 디코딩하는지 재현한다. 그다음 test_e2e.js로 라이브 adaptive↔legacy 프레임이 동일한지 + 대역폭 절감률을 측정하라.
codec.py에 새 인코딩(예: 행 단위 RLE)을 TAG 3으로 추가하고, codec.js 디코더에 대응 분기를 넣는다. 키프레임/하위호환을 깨지 않도록 후보 선택 로직에 끼워 넣고, 과제 4의 검증 스위트로 여전히 byte-exact인지 증명하라. (인코더/디코더 한 쌍을 동시에 다루는 진짜 코덱 작업)
6주 코스 — NumPy 영상 처리부터 실시간 코덱·동기까지.
| 주차 | 주제 | 실습 · 참고 |
|---|---|---|
| 1주차 | OpenCV + NumPy 영상 처리 — 프레임 추출·리사이즈·벡터 인덱싱 | VideoDecoder·AsciiMapper 정독 · 과제 1·3 |
| 2주차 | WebSocket 실시간 스트리밍 — 바이너리 프레임·INIT 핸드셰이크 | stream_server.py websocket_endpoint · FastAPI WS 문서 |
| 3주차 | Canvas 2D 렌더링 — fillText/ImageData·제로카피·선택 레이어 | app.js renderFrame·buildCanvas · 과제 5(E) |
| 4주차 | 압축 기초 — zlib/deflate·RLE·델타 인코딩·키프레임 | codec.py 정독 · 미니 코덱 구현(과제 2) |
| 5주차 | 적응형 시스템 — 상황별 정책 선택·크로스언어 byte-exact 테스트 | codec.js + experiments/ 재현(과제 4) |
| 6주차 | 실시간 A/V 동기 — 마스터 클럭·지터 버퍼·FPS 데시메이션 | 동기 데모 구현 · grab() 데시메이션 분석 |
본문·소스에 나온 용어 빠른 참조.
| 용어 | 의미 |
|---|---|
| 그리드 (grid) | 영상을 줄인 문자 격자. cols×rows, 한 칸(cell)이 한 글자/픽셀에 대응. |
| AsciiMapper | 밝기→문자 LUT + 트루컬러 RLE를 담당하는 변환 클래스. |
| 양자화 (qb) | 색 하위 비트를 깎아 같은 색 셀을 늘리는 것((rgb>>qb)<<qb). 압축률↑. |
| 적응형 코덱 | 프레임마다 RAW/ZLIB/DELTA 중 가장 작은 인코딩을 골라 1바이트 태그로 표시. |
| DELTA | 이전 프레임 대비 바뀐 셀의 인덱스+값만 보내는 인코딩. 정적 화면에 강함. |
| 키프레임 (keyframe) | 48프레임마다 전체를 강제 전송 — 패킷 유실·늦은 접속 재동기용. |
| tolerance (손실 델타) | 색 셀이 이 오차를 넘을 때만 재전송. 0=무손실, 문자 평면은 항상 정확. |
| 마스터 클럭 | A/V 동기 기준 시계. ASCILINE은 오디오 currentTime을 사용. |
| 지터 버퍼 | 도착 간격 불균일을 흡수하는 작은 프레임 큐(frameBuffer). |
| FPS 데시메이션 | 고FPS 소스를 grab()로 프레임 스킵해 30fps로 떨궈 CPU 절감. |
| 제로카피 | 중간 복사 없이 ImageData에 직접 픽셀을 채우는 최적화(픽셀 모드). |
| 선택 레이어 | Canvas 위 투명 <pre> — 그린 글자를 드래그 선택·복사 가능하게. |
| INIT 핸드셰이크 | 접속 시 서버가 보내는 INIT:fps:mode:cols:rows:pixel:idx 메타. |
| Origin 검증 | localhost/동일출처만 WS 허용 — cross-site WebSocket hijacking 방지. |
| FFmpeg | 서버측 오디오 스트림 생성·볼륨 처리. --vol 0이면 미실행. |
stream_server.py(서버·WS) · app.js(렌더러) · codec.py/codec.js(적응형 코덱) · ascii_video_player2.py(엔진·터미널)