| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- spring mongodb
- java 백준 1509
- kotiln const val
- java 1238
- 백준 1504 java
- Claude Intelij 연결
- java 1509
- go
- spring mongodb switch
- 익명 객체 @transactional
- javav 1676
- mongodb lookup
- java 파티
- spring mongoTemplate switch
- 백준 특정한 최단 경로
- rabbitmq 싱글톤
- Java Call By Refernce
- 백준 연결요소 자바
- kotiln const
- 자바 1676
- kotiln functional interface
- 백준 2252 줄세우기
- 안정해시
- ipfs singletone
- nodejs rabbitmq
- Spring ipfs
- java 팩토리얼 개수
- ipfs bean
- 자바 백준 팩토리얼 개수
- spring mongoTemplate
- Today
- Total
공부 흔적남기기
브라우저에서 웹캠 영상을 H.264로 인코딩하기 (2) 본문
WebCodecs VideoEncoder를 이용한 실시간 영상 인코딩
웹캠 영상을 모바일 브라우저로 실시간 전송하는 프로젝트를 진행하면서 먼저 MJPEG 방식으로 간단한 스트리밍을 구현했다.
구조는 다음과 같았다.
웹캠
↓
MediaStream
↓
Canvas
↓
JPEG
↓
HTTP
↓
NestJS
↓
MJPEG Stream
↓
모바일 브라우저
MJPEG 방식은 구현하기 상당히 간단했다.
하지만 한 가지 문제가 있었다.
프레임마다 JPEG 전체 이미지를 전송하기 때문에 데이터량이 상당히 크다.
그래서 다음 단계로 H.264 기반 영상 인코딩을 직접 구현해보기로 했다.
1. WebCodecs란?
브라우저에는 영상과 오디오를 직접 인코딩/디코딩할 수 있는 WebCodecs API가 있다.
이번에는 그중 VideoEncoder를 사용했다.
간단하게 생각하면 다음과 같다.
VideoFrame
↓
VideoEncoder
↓
EncodedVideoChunk
VideoFrame은 하나의 영상 프레임이고,
VideoEncoder는 이 프레임을 H.264 등의 코덱으로 압축한다.
그리고 인코딩 결과가 EncodedVideoChunk로 나온다.
2. VideoEncoder 생성
먼저 VideoEncoder를 생성했다.
const encoder = new VideoEncoder({
output: (chunk, metadata) => {
console.log("encoded chunk", chunk);
console.log("metadata", metadata);
},
error: (error) => {
console.error("encoder error:", error);
},
});
여기서 중요한 것은 output이다.
인코딩이 완료되면 브라우저가 이 콜백을 호출한다.
VideoFrame
↓
encode()
↓
VideoEncoder
↓
output()
↓
EncodedVideoChunk
3. H.264 인코더 설정
다음으로 configure()를 호출했다.
encoder.configure({
codec: "avc1.42E01F",
width: 640,
height: 480,
bitrate: 2_000_000,
framerate: 30,
});
각 옵션은 다음과 같다.
옵션의미
| codec | 사용할 영상 코덱 |
| width | 인코딩 영상 너비 |
| height | 인코딩 영상 높이 |
| bitrate | 목표 비트레이트 |
| framerate | 목표 프레임레이트 |
여기서 2_000_000은 약 2Mbps이다.
4. 처음 발생한 에러
처음에는 다음과 같이 설정했다.
codec: "avc1.42E01E",
width: 1280,
height: 720,
그런데 다음 에러가 발생했다.
NotSupportedError:
The provided resolution (1280x720) has a coded area
(1280*720=921600) which exceeds the maximum coded area
(414720) supported by the AVC level (3.0)
코덱 문자열에 지정된 AVC Level에서 해당 해상도를 지원하지 않는 문제였다.
그래서 다음과 같이 변경했다.
codec: "avc1.42E01F"
이후 1280×720 설정에서 발생하던 해당 에러는 사라졌다.
다만 실제 웹캠에서 받아온 영상은 640×480이었다.
VideoFrame {
format: "I420",
codedWidth: 640,
codedHeight: 480,
...
}
따라서 학습 과정에서는 실제 프레임 크기에 맞춰 다음처럼 설정했다.
width: 640,
height: 480,
5. VideoFrame 만들기
처음에는 loadedmetadata 이벤트를 사용했다.
웹캠은 getUserMedia()가 비동기로 동작하기 때문에 <video> 요소가 존재한다고 해서 바로 영상 프레임을 만들 수 있는 것은 아니다.
따라서 영상 메타데이터가 준비된 이후 VideoFrame을 생성했다.
const createFrame = () => {
const frame = new VideoFrame(video);
console.log("videoFrame:", frame);
frame.close();
};
video.addEventListener("loadedmetadata", createFrame);
실제로 다음과 같은 VideoFrame이 생성됐다.
VideoFrame {
format: "I420",
timestamp: 31997,
codedWidth: 640,
codedHeight: 480,
...
}
여기서 중요한 것은 VideoFrame 생성 자체가 성공했다는 것이다.
I420은 YUV 4:2:0 계열의 영상 포맷이다.
6. VideoFrame → H.264 인코딩
이제 실제 인코딩을 수행했다.
const frame = new VideoFrame(video);
encoder.encode(frame);
frame.close();
encoder.encode(frame)을 호출하면 비동기적으로 인코딩이 진행되고, 완료되면 앞에서 등록한 output 콜백이 호출된다.
output: (chunk, metadata) => {
console.log("encoded chunk", chunk);
}
결과적으로 브라우저에서 실제 H.264 데이터가 생성되는 것을 확인할 수 있었다.
7. 한 프레임이 아니라 계속 인코딩하기
한 번만 encode()하면 당연히 한 프레임만 인코딩된다.
실시간 웹캠 스트리밍을 위해서는 새로운 영상 프레임이 들어올 때마다 VideoFrame을 만들어야 한다.
여기서는 requestVideoFrameCallback()을 사용했다.
const startEncoding = () => {
const encodeFrame = () => {
const frame = new VideoFrame(video);
encoder.encode(frame);
frame.close();
video.requestVideoFrameCallback(encodeFrame);
};
video.requestVideoFrameCallback(encodeFrame);
};
video.addEventListener("loadedmetadata", startEncoding);
역할을 구분하면 다음과 같다.
loadedmetadata
↓
영상 준비 완료
↓
requestVideoFrameCallback()
↓
새로운 영상 프레임
↓
VideoFrame 생성
↓
encoder.encode()
↓
EncodedVideoChunk
↓
다음 영상 프레임 대기
↓
반복
즉,
loadedmetadata는 시작 조건이고
requestVideoFrameCallback은 실제 프레임 처리를 반복하기 위한 콜백이다.
8. Key Frame과 Delta Frame
인코딩 결과를 확인하면서 다음과 같은 값을 확인할 수 있었다.
console.log(chunk.type);
console.log(chunk.byteLength);
chunk.type은 크게 다음과 같이 나온다.
key
delta
delta
delta
delta
...
여기서:
key → KEY Frame
delta → Delta Frame
으로 생각할 수 있다.
H.264에서는 일반적으로 KEY Frame은 독립적으로 복원할 수 있는 기준 프레임이고, 이후 Delta Frame들은 이전 프레임을 참조해 변화된 부분을 표현한다.
따라서 모든 프레임이 KEY Frame일 필요가 없다.
예를 들어:
KEY
↓
DELTA
↓
DELTA
↓
DELTA
↓
DELTA
↓
KEY
↓
DELTA
↓
...
와 같은 형태가 된다.
9. 실제 크기 비교
실제로 브라우저 콘솔에서 크기를 확인해보니 대략 다음과 같았다.
KEY Frame ≈ 24KB
Delta Frame ≈ 4~10KB
KEY Frame은 화면 전체를 독립적으로 표현해야 하기 때문에 상대적으로 크기가 크다.
반면 Delta Frame은 이전 프레임과 비교해 변화된 내용을 중심으로 표현하기 때문에 상대적으로 작다.
웹캠이 고정되어 있고 사람만 움직이는 상황이라면 이런 차이가 더욱 잘 나타난다.
10. 기존 MJPEG와 비교
앞서 구현했던 MJPEG에서는 640×480 JPEG 한 장이 대략 28~29KB 정도였다.
당시에는 약 10FPS로 전송했다.
JPEG
약 28.5KB × 10FPS
≈ 285KB/s
비트 단위로 환산하면:
285KB/s × 8
≈ 2.28Mbps
만약 같은 JPEG를 30FPS로 전송한다면:
28.5KB × 30
≈ 855KB/s
855KB/s × 8
≈ 6.84Mbps
반면 H.264에서는 KEY Frame 이후 Delta Frame을 사용하기 때문에 프레임마다 JPEG 전체를 전송하는 방식보다 훨씬 효율적으로 데이터를 표현할 수 있다.
현재 테스트에서 H.264는 대략:
KEY ≈ 24KB
DELTA ≈ 4~10KB
수준의 데이터가 발생했다.
단, H.264는 프레임마다 크기가 달라지기 때문에 정확한 비교를 위해서는 일정 시간 동안 실제 chunk.byteLength를 모두 측정해 평균 비트레이트를 계산해야 한다.
11. 현재까지 구현한 구조
현재까지 브라우저 안에서 다음 단계까지 구현했다.
┌───────────────┐
│ Webcam │
└───────┬───────┘
│
▼
┌───────────────┐
│ MediaStream │
└───────┬───────┘
│
▼
┌───────────────┐
│ <video> │
└───────┬───────┘
│
│ requestVideoFrameCallback
▼
┌───────────────┐
│ VideoFrame │
└───────┬───────┘
│
│ encode()
▼
┌───────────────┐
│ VideoEncoder │
│ H.264 │
└───────┬───────┘
│
▼
┌────────────────────┐
│ EncodedVideoChunk │
│ KEY / DELTA │
└────────────────────┘
이제 다음 단계는 이 EncodedVideoChunk를 브라우저 안에서 끝내지 않고 서버로 보내는 것이다.
12. 다음 단계
다음 목표는 다음과 같다.
Webcam
↓
VideoFrame
↓
H.264 VideoEncoder
↓
EncodedVideoChunk
↓
WebSocket
↓
NestJS Media Server
↓
Mobile Browser
특히 여기서부터는 단순히 데이터를 보내는 것뿐만 아니라,
- KEY Frame과 Delta Frame의 관계
- H.264 NAL Unit
- SPS / PPS
- WebSocket Binary 전송
- 서버에서 여러 클라이언트로 Broadcast
- 모바일 브라우저에서 H.264 디코딩
- 실시간 지연시간 관리
등을 고려해야 한다.
이번 단계에서는 우선 브라우저에서 웹캠 영상을 H.264로 인코딩하고 실제 EncodedVideoChunk가 생성되는 것까지 확인했다.
https://github.com/minkik715/mk-webcam-stream