
어제 한 것
Gavarnie 프로젝트 기획
Docker Compose를 통한 로컬 인프라 구성 (MySQL, Redis, MongoDB, MinIO, Nginx)
환경 변수 세팅, MinIO, Nginx 동작 검증 (media 버킷 생성 및 download 정책 적용)
샘플 업로드 후 직접
이 부분에 대해서는 좀 더 스터디가 필요함.
백엔드 애플리케이션 (API, WORKER) 생성
인프라 헬스 체크 구성
아직 잘 이해하지 못한 부분 정리
MinIO
오픈소스 기반의 오브젝트 스토리지 시스템인데, 쉽게 말해서 AWS S3와 똑같은 방식으로 파일(객체)을 저장/관리할 수 있는 자체 서버라고 보면 된다.
영상, 음원 같은 대용량 미디어 파일을 효율적으로 저장 가능케해주고, S3 API와 호환이되어서 나중에 AWS S3으로 이전할 때 코드 수정이 거의 필요 없다는 장점이 있다. 또한 로컬 개발 환경에서도 클라우드 스토리지처럼 사용할 수 있다.
Gavarnie 프로젝트는 영상/음원 스트리밍 서비스이므로 개발이나 서비스 중에 영상 파일을 어딘가에 안전하게 저장하고, 클라이언트가 요청할 때 안정적으로 제공해줘야한다. 하지만 서버 디렉토리에 저장하는 방식은 확장성과 성능 그리고 안정성에 문제가 생긴다. 그래서 MinIO와 같은 오브젝트 스토리지를 사용해서 파일 저장소와 서비스 서버를 깔끔하게 분리하려는 것이다.
정리하자면,
1. S3 AWS와 비슷한거고, S3와 호환이 되서 나중에 이전할 때 코드 수정이 거의 필요 없다.
2. 파일 저장소와 서비스 서버를 깔끔하게 분리한다.
HLS 와 m3u8 그리고 ts 세그먼트
HLS (HTTP Live Streaming) 은 애플이 만든 영상 스트리밍 방식으로, 긴 영상을 잘게 쪼개서 작은 조각(ts 세그먼트)로 나눠 전송하고, 그 조각들의 목록을 플레이 리스트(m3u8 파일)로 관리하는 방식이다.
영상 전체를 한 번에 다운받는 대신, 조각 단위로 불러오므로 빠른 재생 시작이 가능하며, 네트워크 상황에 따라 낮은 화질/높은 화질로 동적 전환이 가능하다.
흐름은 아래와 같다.
- 원본 영상(mp4 등)을 몇 초 단위로 잘라서, .ts(Transport Stream)파일로 나눈다.
- 잘라낸 ts 파일들의 순서를 담은 텍스트 파일형태인 .m3u8을 생성한다.
- 플레이어(브라우저, 앱)는 .m3u8을 읽고 순서대로 ts 조각을 다운받아 이어 붙여 재생한다.
Download 정책
MinIO 같은 오브젝트 스토리지에서 파일 접근 권한을 어떻게 줄지 설정하는 규칙이다.
- 읽기 전용(Download 전용): 업로드는 못 하고, 파일 읽기만 가능
- 일시적 접근(Presigned URL): 특정 시간 동안만 유효한 링크 제공
- 범위 요청 지원(Range Request): 파일 전체가 아니라 필요한 부분만 다운로드 가능
Nginx
Nginx는 초고성능 웹 서버 및 리버스 프록시 서버이다.
일반적으로 정적 파일 서비스(이밎, 영상, CSS, JS 같은 파일을 빠르게 제공), 리버스 프록시(클라이언트 요청을 받아 백엔드 서버나 다른 서비스 (MinIO) 로 전달 하는 용도로 많이 쓰인다.
현재 Gavarnie 프로젝트에서의 역할은 MinIO가 직접 파일을 제공할 수도 있지만, Nginx를 앞단에 두어 프록시 역할을 맡고 있는데,
장점은 아래와 같다.
- 도메인과 포트를 정리해주고 (클라이언트가 media.gavarnie.com 등으로 접근 하도록),
- 캐싱 (같은 ts파일 요청 시 더 빠르게 제공),
- HTTPS(443) 적용(안전한 스트리밍 제공),
- CORS 정책 제어(특정 도메인에서만 접근 허용 가능)
결국 개발 후 배포할 때는 MinIO -> Amazon S3, Nginx -> CloudFront(+ ALB) 로 대체되게 될 것이다.
오늘 작업 중에 필요한 개념
MIME 타입(Multipurpose Internet Mail Extensions type)
데이터의 형식(타입)을 표현하는 문자열 규칙
- 형식: type/subtype
- type → 큰 분류 (text, image, audio, video, application 등)
- subtype → 구체적 포맷 (html, png, mp4, json 등)
즉, 클라이언트와 서버가 데이터를 주고받을 때 “이게 이미지야, 그중에서도 PNG야” 라는 걸 알려주는 표기법이다.
예시 : ( image/jpeg → JPEG 이미지, audio/mpeg → MP3 오디오, video/mp4 → MP4 비디오)
Presigned URL
- 말 그대로 미리 서명된 URL이다.
- S3와 같은 오브젝트 스토리지는 파일에 접근하려면 IAM 권한이나 버킷 정책이 필요하다.
- 하지만 모든 사용자에게 IAM 권한을 줄 수는 없으니, 대신 서버가 이 파일을 일정 시간 동안 특정 동작만 할 수 있도록 권한이 들어간 URL을 만들어준다. 그 URL을 받은 사용자는 따로 로그인이나 키를 몰라도, 정해진 시간 동안은 파일을 읽거나 업로드 할 수 있게 된다.
- 서버가 S3/MinIO SDK를 사용해서 Presigned URL을 생성한다 (GET 5분 가능, PUT 권한 10분 등)
- 생성된 URL에는 서명과 만료시간이 포함되어있고 클라이언트는 이 URL을 그대로 사용해서 S3나 MinIO에 직접 요청한다.
- 장점
- 보안: 버킷을 전부 공개해놓는 건 위험하다. Presigned URL은 짧은 시간만 열려 있으니, 링크를 공유해도 금방 무효화된다.
- 편리함: 클라이언트가 직접 스토리지와 통신하므로, 백엔드 서버에 파일 업로드/다운로드 부담이 줄어든다.
- 예: 사용자가 영상을 올릴 때 → 백엔드가 Presigned URL 발급 → 클라이언트가 그 URL에 바로 업로드
- 권한 위임: 백엔드는 모든 권한을 갖고 있고, 사용자는 “특정 파일에 대한 제한된 권한”만 받는 구조라 관리가 쉽다.
1. 업로드 Presigned URL 발급 API
S3 (MinIO) 클라이언트 모듈
- S3_CLIENT 심볼 지정 (export const S3_CLIENT = Symbol('S3_CLIENT');)
- Symbol은 고유한 식별자를 만들기 위해 사용하는 자료형이다.
- 자바스크립트(그리고 타입스크립트)에서 Symbol은 유일하고 변경 불가능한 값을 만드는 기본 자료형
- Symbol('S3_CLIENT') 를 실행하면 새로운 심볼 값이 하나 생성되며 같은 문자열 'S3_CLIENT'를 넣더라도, 심볼은 매번 고유
- NestJS에서는 종종 **DI 토큰(Dependency Injection Token)**으로 심볼을 사용한다.
- NestJS는 의존성을 주입할 때 “토큰”을 사용하는데, 이 토큰은 문자열로도 가능하지만, 문자열은 충돌 위험이 있다.
Symbol을 사용하고 @Inject() 로 주입을 하는 이유
NestJS 컨테이너는 토큰 -> 인스턴스 로 의존성을 저장한다.
토큰(Token) ──▶ 실제 값/인스턴스(Value)
무엇을 주입받고 싶은지를 토큰으로 알려주면, 컨테이너가 그 토큰에 연결된 값을 넣어주는 방식인 것인데, S3_CLIENT 심볼이 그 토큰(키)가 되는 것이고, useFactory가 만든 new S3Client(...)는 그 값(인스턴스)가 되는 것이다.
그래서 service 단에서 @Inject(S3_CLIENT) 라 하면 그 S3_CLIENT 토큰으로 등록된 인스턴스를 달라는 의미인 것이다.
왜 이렇게 하냐면 S3Client는 Nest가 만든 @Injectable() 서비스 클래스가 아닌, 외부 라이브러리 객체이다. (@aws-sdk/client-s3)
Nest는 S3Client라는 타입 이름만 보고 어떤 인스턴스를 줄지 모르기 때문에 토큰을 직접 만들어 알려줘야 하는 것이다.
그래서 모듈에서 S3_CLIENT라는 고유한 식별자를 생성해서 Nest 컨테이너에서 등록하거나 조회할 유일한 값을 생성하고, 인스턴스를 만드는 방법을 정의해두는 것이다.
providers: [
{
provide: S3_CLIENT, // ← 토큰(키)
useFactory: (config) => // ← 인스턴스를 만드는 방법
new S3Client({...}) // ← 실제 값(인스턴스)
}
],
exports: [S3_CLIENT] // ← 다른 모듈에서도 이 토큰으로 꺼내 쓸 수 있게 공개
그리고 S3Client를 사용할 서비스에서 S3_CLIENT라는 이름의 식별자로 의존성을 주입받아 사용하게 되는 것이다.
이렇게 사용했을 때의 장점은,
- 구성/생성 위치 일원화: S3Client 옵션(엔드포인트, 자격증명 등)을 모듈 한 곳에서 생성/검증.
- 재사용/싱글톤: 앱 전역에서 같은 인스턴스 공유 → 커넥션/리소스 낭비 줄임.
- 테스트 용이: 테스트에서 S3_CLIENT 토큰에 가짜(mock) 클라이언트를 바인딩해 갈아끼우기 쉽다.
- 충돌 방지: 문자열 토큰보다 Symbol이 절대 충돌하지 않아 안전.
PresignedAPI의 목적과 역할, 그리고 동작
- 외부 라이브러리 객체(S3Client)는 Nest가 자동으로 관리할 수 없으니
- export const S3_CLIENT = Symbol('S3_CLIENT') 같은 고유 토큰을 만들어 등록합니다.
- 그리고 @Inject(S3_CLIENT)로 의존성을 주입받습니다.
- getSignedUrl 같은 AWS SDK의 도우미 함수를 사용해
- S3/MinIO에 직접 PUT 요청을 보낼 수 있는 presigned URL을 생성합니다.
- 이 presigned URL을 클라이언트에게 API 응답으로 돌려주면
- 클라이언트는 파일 바디를 서버로 거치지 않고, 스토리지에 바로 업로드할 수 있습니다.
- 서버는 트래픽/메모리 부담을 줄이고, 클라이언트는 대용량 파일도 안정적으로 업로드 가능해집니다.
2. BullMQ Queue 모듈
BullMQ
- Redis 기반의 Job Queue(작업 큐) 라이브러리입니다.
- Node.js 환경에서 비동기 작업 처리를 쉽게 만들 수 있게 해줍니다.
- BullMQ는 단순 큐보다 강력해서:
- 지연 작업(Delay)
- 재시도/실패 처리
- 동시성 제어
- 이벤트 기반 처리
- 작업 우선순위
등을 지원
즉시 사용자 응답과, 시간이 오래 걸리는 백그라운드 작업을 분리하는 게 필수이고, Gavarnie에서 BullMQ가 맡게 될 역할은 바로 이 백그라운드 처리이다.
1) 영상 업로드 후 처리 파이프라인
- 사용자가 영상을 업로드 → 서버는 업로드 성공 응답만 빠르게 반환
- 이후 BullMQ Job으로:
- 영상 트랜스코딩 (원본 → HLS .m3u8 + .ts 세그먼트 생성)
- 썸네일 추출
- 메타데이터 분석 (길이, 해상도, 코덱 등)
- 이 작업은 시간이 오래 걸리므로 **큐에 넣고 워커(worker)**가 순차/병렬 처리
2) 지연 처리/스케줄링
- 예: “업로드 후 1분 뒤 자동 검증 작업 실행”
- BullMQ는 delay 옵션으로 특정 시간 이후에 실행 가능.
3) 재시도/실패 처리
- 영상 인코딩 중간에 에러 발생 → BullMQ가 자동으로 재시도
- 계속 실패하면 Failed Queue로 보내고, 관리자/로깅에 활용.
4) 알림/비동기 후속 작업
- 영상 처리 완료 후: DB 상태 업데이트 → 알림 Job 큐에 넣기 → 유저에게 “영상 준비 완료” 푸시
- 대규모 트래픽 상황에서도 Job Queue 덕분에 안정적으로 순서 보장/처리 분산
5) 확장성 & 워커 분리
- 웹 API 서버와 Worker 서버를 분리 배포 가능
- API 서버는 사용자 요청만 처리 → 빠른 응답
- Worker 서버는 BullMQ 큐만 소비하면서 무거운 작업 수행 → 서버 부하 분산
주의
BullMQ의 Queue 옵션에서 connection은 문자열이 아니라
ConnectionOptions(host/port 등) 혹은 ioredis 인스턴스여야한다.
useFactory: (config: ConfigService) => {
const connection = config.get<string>('REDIS_URL')!;
return new Queue('transcode', { connection, prefix: 'bull' });
},
가 아닌, 아래의 형태
useFactory: (config: ConfigService) => {
const url = config.getOrThrow<string>('REDIS_URL');
const connection = new Redis(url, {
maxRetriesPerRequest: null,
});
return new Queue('transcode', { connection, prefix: 'bull' });
},
영상 업로드의 흐름은 클라이언트가 서버로부터 presignedURL을 받아서 S3, MinIO에 업로드를하고, 완료 API를 호출하면 서버에서 DB, Redis에 상태를 업데이트 해주고, 트랜스코드 처리를 시작하게 된다.
const worker = new Worker(
'transcode',
async (job) => {
const { mediaId, srcKey } = job.data;
this.logger.log(`job=${job.id} mediaId=${mediaId} srcKey=${srcKey}`);
//... FFmpeg 변환 실행
return true;
},
{ connection, prefix: 'bull', concurrency: 1 },
);
new Worker(queueName, processor, options)
- queueName: 'transcode' 큐를 구독 (여기서 Job을 가져옴)
- processor: Job을 처리하는 함수
- job.data에 큐에 넣을 때 지정한 { mediaId, srcKey }가 들어옴
- 여기서 나중에 FFmpeg를 실행해 HLS 세그먼트(.m3u8/.ts)를 만들 예정
- options:
- connection: 위에서 만든 Redis 연결
- prefix: 'bull': Redis 키 프리픽스
- concurrency: 1: 한 번에 하나의 Job만 처리 (FFmpeg 무거워서 직렬 처리)
FFmpeg = 오픈소스 멀티미디어 처리 툴킷
- 역할
- 동영상/오디오 인코딩, 디코딩
- 포맷 변환 (ex: mp4 -> .ts)
- 스트리밍 준비 (HLS/DASH 세그먼트 쪼개기)
- 썸네일 추출, 오디오 추출 등
BullMQ에서 Worker / QueueEvents
1) Worker
- 정의: 큐에 쌓인 Job(작업)을 실제로 처리하는 실행자.
- 동작:
- 누군가 queue.add('작업명', 데이터)로 Job을 넣으면
- Worker가 Redis를 감시하다가 Job을 가져와
- 우리가 작성한 processor 함수(async 콜백)를 실행
2) QueueEvents
- 정의: 큐의 상태 이벤트를 모니터링하는 객체.
- Worker는 "일을 한다"라면, QueueEvents는 "일의 결과를 알려준다".
- 예:
- completed: Job 성공했을 때 발생
- failed: Job 실패했을 때 발생
- stalled: Job이 멈췄을 때 발생
IMPORTANT! Eviction policy is allkeys-lru. It should be "noeviction"
Redis는 in-memory 데이터베이스라서, 설정된 메모리 한도(maxmemory)를 초과하면 무언가를 버려야(evict) 합니다.
이때 어떤 키를 버릴지 정하는 전략이 Eviction Policy이다.
- allkeys-lru → 모든 키 중에서 가장 덜 사용된(LRU) 키부터 지운다
- volatile-lru → TTL 설정된 키 중 덜 사용된 것부터 지운다
- allkeys-random → 무작위로 지운다
- noeviction → 절대 지우지 않는다. 메모리가 꽉 차면 OOM command not allowed 에러를 던진다.
- BullMQ 같은 작업 큐에서 Redis를 쓰는 경우,
Job 데이터가 LRU 정책에 의해 지워지면 작업이 증발해 버린다. - 즉, “큐에 넣었는데 Redis가 메모리 관리 때문에 마음대로 지워버림 → Job 유실” 문제가 생김.
- 그래서 BullMQ 문서에서도 반드시 noeviction 으로 설정하라고 경고하는 것이다.
typeORM 에러
@Column({ length: 512, nullable: true })
hlsKey!: string | null;
TypeORM은 컬럼 타입을 데코레이터 메타데이터로 추론한다.
hlsKey: string | null처럼 유니온 타입이 들어가면 메타데이터가 Object로 떨어져서,
DataTypeNotSupportedError: Data type "Object" in "Media.hlsKey" …
가 발생한다.
@Column({ type: 'varchar', length: 512, nullable: true })
hlsKey!: string | null;
와 같이 해당 컬럼에 정확한 타입을 명시해주면 된다.
S3 유틸 개발
목적: MinIO(S3)에서 원본 다운로드 및 HLS 산출물 업로드
import { createReadStream, createWriteStream, readdirSync, statSync } from 'fs';
import { pipeline } from 'stream';
import { promisify } from 'util';
import { join } from 'path';
const pipe = promisify(pipeline);
코드 일부 중 평소에 많이 사용해보지 않은 부분 위주로 정리를 해보려 한다.
- createReadStream(path)
- 특정 파일을 읽기 전용 스트림으로 연다.
- 큰 파일을 한 번에 읽지 않고 조금씩 잘라서 처리할 수 있다.
- createWriteStream(path)
- 특정 파일을 쓰기 전용 스트림으로 연다
- 데이터를 한번에 쓰지 않고, 조금씩 이어 붙여 저장할 수 있다.
- readdirSync(path)
- 디렉토리 안의 파일/폴더 이름들을 동기적으로 배열로 반환한다.
- 예: ['file1.txt', 'file2.txt', 'subdir']
- statSync(path)
- 특정 경로(파일/폴도)의 상세 정보 (Stat 객체)를 동기적으로 가져온다.
- 여기서 얻는 정보: 파일 크기, 수정 시각, 폴더 여부 등
Stream(스트림)이란?
사전적 의미로는 개울, 시내, (액체의) 줄기, 줄줄 흐르다. 이런 느낌인데 프로그래밍적으로도 스트림은 연속성을 갖는 흐름을 의미한다.
큰 파일을 잘게 쪼개서 연속적으로 줄지어 보내는 것이다.
처음에는 Buffer과 계속 햇갈렸는데, 사실 관심을 가지고 제대로 알려고하면 햇갈릴 수 없는 개념이다.
Buffer(버퍼)는 메모리(RAM)에 잠깐 데이터를 담아두는 공간이다. 파일이나 네트워크에서 데이터를 읽을 때, 운영 체제는 데이터를 일정 크기 단위로 버퍼에 먼저 올려둔다. Node.js에서는 Buffer 객체로 이런 이진 데이터를 다룰 수 있다.
기본적으로 버퍼링은 영상 전체를 다운로드해야만 재생할 수 있는 방식이다.
즉, 모든 데이터를 버퍼(메모리)에 담아둔 후에야 실행할 수 있는 것.
반면!
Stream(스트림)은 데이터를 흐르는 물줄기처럼, 조각(chunk)단위로 계속 이어받아 처리하는 방식이다.
한 번에 다 읽어들이는 것이 아니라, 읽는 동시에 가공하거나 전송할 수 있다.
영상을 조각(chunk) 단위로 받아오면서 도착하는 즉시 재생한다. 유튜브의 경우 앞부분 몇 초씩 받아와서 버퍼에 저장한 뒤, 곧바로 재생을 시작한다. 그 뒤에도 네트워크로 들어오는 데이터를 계속 스트리밍하면서 재생을 이어간다.
즉, chunk가 곧 buffer 이라 볼 수 있고, 그 것들의 연속이 stream인 것이다. *(물론 chunk가 buffer 일 필요는 없다. chunk는 단순히 작텍스트든, 바이너리 데이터든 상관없이 작은 조각을 의미한다.)
Node.js의 스트림API는 데이터를 조각(chunk) 단위로 읽고 쓰는 방식이다.
예를 들면, 영화 파일이나 로그 파일과 같이 큰 데이터를 한 번에 메모리에 올리지 않고, 조금씩 처리하는 것이다.
장점
- 메모리 절약 ( 큰 파일을 한 번에 안 읽음)
- 빠른 처리 ( 읽는 동시에 다른 작업 가능)
스트리밍 데이터
https://aws.amazon.com/ko/what-is/streaming-data/
스트리밍 데이터란 무엇인가요? - 스트리밍 데이터 설명 - AWS
스트리밍 데이터는 짧은 대기 시간 처리를 목표로 계속해서 증분하는 방식으로 내보내지는 대용량 데이터입니다. 조직은 보통 몇 바이트부터 메가바이트(MB)에 이르는 다양한 크기의 메시지, 레
aws.amazon.com
Pipeline
파이프라인은 Node.js에서 제공하는 함수로, 여러 스트림을 안전하게 연결해주는 역할을 한다.
Promisify
Node.js의 콜백 기반 함수를 Promise 기반 함수로 바꿔준다.
pipeline()은 콜백 스타일인데, promsify로 감싸서 async/await 구문에서 사용할 수 있도록 하는 목적이다.
S3Client 라이브러리를 통해 개발된 S3 유틸에는 두가지 메서드가 존재한다.
원본 내려받기(스트리밍 저장)
- 워커가 S3/MinIO에서 원본을 스트리밍으로 받아 로컬 파일에 저장
디렉토리 업로드 (멀티파트 업로드)
- 워커가 FFmpeg가 만든 HLS 결과물 디렉토리(m3u8/ts)를 S3로 올림
FFmpeg로 변환을 하는 이유
원본 mp4를 그대로 내려주는 방식은:
- 전체 파일을 큰 덩어리로 받게 되고, 네트워크/재생 중 끊김 복구가 어려움
- 여러 해상도/비트레이트 적응(ABR)이 안 됨
- 브라우저/기기별 코덱 호환성 이슈
HLS 변환을 하면:
- 몇 초짜리 조각(.ts) 으로 쪼개져서 빠르게 재생 시작 & 끊겨도 이어받기 쉬움
- 적응형 스트리밍(ABR) 지원 → 네트워크 상태에 맞게 자동 화질 전환
- 표준 재생(m3u8)이라 플레이어 호환성 좋음
- CDN 캐싱에 최적화 (HTTP GET로 조각 단위 캐시)
- 그래서 “업로드 받은 원본 → FFmpeg로 HLS 세그먼트 생성 → 그걸 배포”가 스트리밍의 정석.
trasncodeToHLS 에서 ffmpeg() 세팅 값
try {
await downloadToFile(srcKey, input);
await new Promise<void>((resolve, reject) => {
ffmpeg()
.input(input)
.outputOptions([
'-c:v h264','-c:a aac','-preset veryfast',
`-hls_time ${seg}`,'-hls_list_size 0',
'-hls_segment_filename', `${outDir}/segment_%05d.ts`,
'-f hls','-movflags +faststart','-y',
])
.output(join(outDir, 'index.m3u8'))
.on('start', (cmd) => console.log('FFmpeg:', cmd))
.on('end', () => resolve())
.on('error', reject)
.run();
});
단일 동영상 파일을 HLS(= m3u8 + ts 세그먼트)로 변환해서 MinIO(S3 호환 스토리지)에 올리고, 최종 재생 진입점(URL)(= …/index.m3u8)을 돌려주는 전체 파이프라인 중 일부인데, 풀어서 보면 아래와 같다.
- .input(input): 방금 내려받은 원본 파일을 입력.
- .outputOptions([...]): FFmpeg에 전달할 옵션들:
- -c:v h264 : 비디오 코덱을 H.264로. (웹/모바일 호환성 높음)
- -c:a aac : 오디오 코덱을 AAC로. (역시 범용)
- -preset veryfast : 인코딩 속도/압축률 트레이드오프.
- ultrafast ↔ veryfast ↔ medium ↔ slow …(느릴수록 용량↓ 품질↑)
- -hls_time ${seg} : HLS 세그먼트 길이(초). 위에서 정한 6초 등.
- -hls_list_size 0 : 플레이리스트에 모든 세그먼트를 보관(라이브가 아닌 VOD에 적합).
- -hls_segment_filename ${outDir}/segment_%05d.ts : 세그먼트 파일명 패턴
(예: segment_00001.ts, segment_00002.ts …) - -f hls : 출력 포맷을 HLS로 설정(= m3u8 + ts).
- -movflags +faststart : MP4의 moov를 앞쪽으로 옮겨 빠른 시작(진짜 MP4 낼 때 유용, HLS에선 큰 영향 X, 그래도 무해)
- -y : 출력 덮어쓰기 허용.
- .output(join(outDir, 'index.m3u8')): HLS 마스터/미디어 플레이리스트 생성 위치.
- .on('start' | 'end' | 'error'): 시작/성공/실패 이벤트 핸들링.
- .run(): FFmpeg 실행.
최종적으로 클라이언트(플레이어)에게 **재생 진입점(m3u8 경로)**만 전달하면, 플레이어가 내부에서 연달아 ts 세그먼트를 가져오게 된다.
'ToyProject' 카테고리의 다른 글
| [Catarie] error-case: audio HLS 변환 실패, 재생실패 (1) | 2025.09.01 |
|---|---|
| [Catarie] HLS와 업로드 흐름 (1) | 2025.08.28 |
| [Catarie] 영상 소셜 네트워크 프로젝트 - 1. 기획과 설계 (2) | 2025.08.25 |
| [nestJS] 티켓 예약 시스템 구축 - 7 (개선, 완) (3) | 2025.08.02 |
| [nestJS] 티켓 예약 시스템 구축 - 6 (통합 테스트) (5) | 2025.07.31 |