Skip to content

๐Ÿš€ HLS๋กœ ์Œ์•… ์ฃผ๊ณ ๋ฐ›๊ธฐ

Kang Chaeryeon edited this page Nov 21, 2024 · 1 revision

Main Flow

  1. Client์—์„œ .m3u8 ํŒŒ์ผ ์š”์ฒญ
  2. Server์—์„œ NCP Object Storage์— ์ ‘๊ทผํ•ด ํ•ด๋‹น m3u8 ํŒŒ์ผ์„ ๊ฐ€์ ธ์˜ด
  3. ๊ฐ€์ ธ์˜จ ํ›„, Server๋Š” ๋ฐฉ์— ์ž…์žฅํ•œ ์‹œ๊ฐ„(TimeStamp) ๊ธฐ์ค€์œผ๋กœ ์•Œ๋งž์€ ์Œ์•…์˜ ํ•„์š”ํ•œ ์„ธ๊ทธ๋จผํŠธ๋“ค๋งŒ ๋‹ด๊ธด .m3u8 ํŒŒ์ผ์„ ์‘๋‹ต
  4. Client๋Š” ์‘๋‹ต๋ฐ›์€ .m3u8 ํŒŒ์ผ์„ ๋ฐ›์•„ HLS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™์œผ๋กœ .ts ์„ธ๊ทธ๋จผํŠธ ์š”์ฒญ์„ ๋ณด๋ƒ„
  5. Server๋Š” NCP Object Stroage์— ์ ‘๊ทผํ•ด ํ•ด๋‹น tsํŒŒ์ผ๋“ค์„ ์‘๋‹ตํ•จ

์ƒ๊ฐํ•ด๋ด์•ผํ•˜๋Š” ์ 

1. ์บ์‹ฑ

ํ˜„์žฌ๋Š” NCP Object Storage์— ์ ‘๊ทผํ•ด์„œ m3u8ํŒŒ์ผ์„ ๊ฐ€์ ธ์˜จ๋‹ค. ์—ฌ๊ธฐ์„œ, ๊ทธ ๋ฐฉ์— ์žˆ๋Š” ๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ํŒŒ์ผ์„ ๊ฐ€์ ธ์˜ค๋Š” API๋ฅผ ์ฐŒ๋ฅผํ…๋ฐ,

์ด๋Š” ์œ ์ €๊ฐ€ N๋ช…์ด๋ผ ๊ฐ€์ •ํ•˜๋ฉด, Object Stroage์— N๋ฒˆ ์ ‘๊ทผํ•œ๋‹ค. ๋”ฐ๋ผ์„œ, ํŠธ๋ž˜ํ”ฝ ์ฆ๊ฐ€์˜ ์šฐ๋ ค๊ฐ€ ์žˆ๋‹ค.

๋”ฐ๋ผ์„œ, ์บ์‹ฑ์„ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค.

import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { S3 } from 'aws-sdk';
import { Cache } from 'cache-manager';

export class MusicService {
  private readonly s3: S3;
  private readonly SEGMENT_DURATION = 2;
  constructor(
    private readonly configService: ConfigService,
    @Inject(CACHE_MANAGER) private cacheManager: Cache, // nestJS์˜ Cache Manager ์‚ฌ์šฉ
    )
    const response = await this.s3.getObject(params).promise();
    originalM3u8 = response.Body.toString();
    await this.cacheManager.set( // ์บ์‹ฑ
          originalM3u8CacheKey,
          originalM3u8,
          3600000,
        );

์บ์‹ฑ์„ ์‚ฌ์šฉํ•˜๋ฉด, ์›๋ณธ .m3u8 ํŒŒ์ผ (๋ชจ๋“  ์„ธ๊ทธ๋จผํŠธ๊ฐ€ ๋“ค์–ด๊ฐ€์žˆ์Œ)์„ ์‚ฌ์šฉํ•˜์—ฌ, ์œ ์ €๋งˆ๋‹ค ์ž…์žฅ์‹œ๊ฐ„์„ ๊ณ ๋ คํ•˜์—ฌ ๋‹ค๋ฅธ m3u8ํŒŒ์ผ์„ ์‘๋‹ตํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

๋˜ํ•œ, segment ํŒŒ์ผ๋“ค๋„ ์›๋ณธ์„ ์บ์‹ฑ์œผ๋กœ ์ €์žฅํ•˜๊ณ , ๊ฐ€์ ธ๋‹ค ์“ฐ๋ฉด ๋  ๊ฒƒ ๊ฐ™๋‹ค.

2. ํŒŒ์‹ฑ ์˜ค๋ฒ„ํ—ค๋“œ

์ดˆ๋ฐ˜์—๋Š”, ์œ ์ €์—๊ฒŒ ํ•„์š”ํ•œ m3u8์„ ์‘๋‹ตํ•ด์ฃผ๋Š” ๊ณผ์ •์—์„œ, m3u8์„ ํŒŒ์‹ฑ ํ›„, ์žฌ๊ตฌ์„ฑํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๊ฒฐ์ •ํ•˜์˜€๋‹ค.

๋‹ค๋งŒ, ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ์˜ ๊ต‰์žฅํ•œ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์šฐ๋ ค๊ฐ€ ๋˜๋Š” ์ƒํ™ฉ์ด์–ด์„œ, ๋Œ€์•ˆ์„ ์ฐพ์•„๋ณด์•˜๋‹ค.

  • Server
@Injectable()
export class MusicService {
  async generateMusicInfo(joinTimeStamp: number): Promise<{
    m3u8Url: string;
    startSegment: number;
  }> {
    const currentSong = await this.findCurrentSong(joinTimeStamp);
    if (!currentSong) {
      throw new Error('No song is currently playing');
    }

    const elapsedTime = joinTimeStamp - currentSong.startTime;
    const elapsedSeconds = Math.floor(elapsedTime / 1000);
    const startSegment = Math.floor(elapsedSeconds / this.SEGMENT_DURATION);

    return {
      m3u8Url: `${currentSong.id}/playlist.m3u8`,
      startSegment
    };
  }
}
  • Client
const hls = new Hls({
  startPosition: startSegment // ์‹œ์ž‘ ์„ธ๊ทธ๋จผํŠธ ์ง€์ •
});
hls.loadSource(m3u8Url);

startPosition ์ด๋ผ๋Š” ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ์ž‘ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ง€์ •ํ•ด์ค„ ์ˆ˜ ์žˆ์—ˆ๊ณ , ์ด๋Š” Server์—์„œ ์‹œ์ž‘๋˜๋Š” ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ •ํ•ด์ฃผ๋ฏ€๋กœ ํŒŒ์‹ฑ ๊ณผ์ •์„ ๊ฑฐ์น˜์ง€ ์•Š์•„๋„ ๋˜์—ˆ๋‹ค.

์—ฌ๊ธฐ์„œ ๋ฌธ์ œโ€ฆ..

์ฐพ์•„๋ณด๋‹ˆ currnet ์ง€์ ์„ ํ™•์ธํ•˜๋Š” api๋ฅผ ํ•œ ๊ฐœ ๋” ์ƒ์„ฑํ•ด์ค˜์•ผํ•˜๋Š” ๊ฒƒ์ด์—ˆ๋‹ค.

๊ทธ๋ƒฅ ํŒŒ์‹ฑํ•˜๋Š” ๊ฒƒ์ด ํ›จ์”ฌ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์ ๋‹ค๊ณ  ๊ฒฐ๋ก ์„ ์ง€์—ˆ๋‹ค. ๊ฒฐ๊ตญ ๊ทธ๋ƒฅ ํŒŒ์‹ฑํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋Œ์•„๊ฐ€์•ผํ•œ๋‹ค..

3. ์ž…์žฅ ์‹œ๊ฐ„ ๊ณ„์‚ฐ

์ž…์žฅ ์‹œ๊ฐ„์— ๊ด€ํ•ด์„œ ์–ด๋–ป๊ฒŒ ๊ณ„์‚ฐ์„ ํ•ด์•ผํ• ์ง€ ๋ง‰๋ง‰ํ–ˆ๋‹ค.

๋ฐฉ ์ž…์žฅ์‹œ๊ฐ„์„ ๊ธฐ๋กํ•œ JoinTimeStamp๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์•„์˜ค์ง€๋งŒ, ์Œ์•… ์‹œ์ž‘ ์‹œ๊ฐ„์„ ๋ฐ›์•„์˜ฌ ๋ฐฉ๋ฒ•์ด ๋– ์˜ค๋ฅด์ง€ ์•Š์•˜๋‹ค.

์œค์„œ๋‹˜๊ป˜ ์—ฌ์ญค๋ณธ ๊ฒฐ๊ณผ, ๊ด€๋ฆฌ์ž๊ฐ€ ๋ฐฉ์„ ์ƒ์„ฑํ•  ๋•Œ ์•„์˜ˆ ์Œ์•…์— ๋Œ€ํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ Redis์— ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์„ ์ œ์•ˆํ•ด์ฃผ์…จ๋‹ค.

์—ฌ๊ธฐ์„œ ํ‚ฅ์€, ์ €์žฅํ•  ๋•Œ, ๊ฐ ์Œ์•…์— ๋Œ€ํ•œ ์‹œ์ž‘ ์‹œ๊ฐ„๋„ ์ €์žฅํ•œ๋‹ค๋Š” ๊ฒƒ.

๊ฒฐ๊ตญ Redis์—๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ํ˜•์‹์œผ๋กœ ์ €์žฅ์ด ๋  ๊ฒƒ ๊ฐ™๋‹ค.

songs: [
    {
      id: "song1",
      startTime: 1710810000000, // 2024-03-19 10:00:00
      duration: 180 // 3๋ถ„
    },
    {
      id: "song2",
      startTime: 1710810180000, // 2024-03-19 10:03:00
      duration: 200 // 3๋ถ„ 20์ดˆ
    }
  ]

๋”ฐ๋ผ์„œ ํ•ด์ค˜์•ผํ•˜๋Š” ์ž‘์—…์€

  1. ์œ ์ €๊ฐ€ ๋“ค์–ด์˜จ ์‹œ๊ฐ„๊ณผ ๊ฐ ๋…ธ๋ž˜์˜ startTIme๋“ค์„ ๋น„๊ตํ•ด์„œ ํ˜„์žฌ ์žฌ์ƒ๋˜์–ด์•ผ ํ•˜๋Š” ์•Œ๋งž์€ ๋…ธ๋ž˜๋ฅผ ์ฐพ๊ณ ,
  2. ๊ทธ ๋…ธ๋ž˜์˜ startTime๊ณผ ๋ฐฉ ์ž…์žฅ ์‹œ๊ฐ„์„ ๋น„๊ตํ•ด์„œ segment ํฌ๊ธฐ(2์ดˆ?)๋กœ ๋‚˜๋ˆˆ ๋’ค ์•Œ๋งž์€ startSegment๋ฅผ ์ง€์ •ํ•ด์ค€๋‹ค.

11/20 ๋ณ€๊ฒฝ์‚ฌํ•ญ

KEY: "rooms:{roomId}:session"
HASH Fields:
- releaseTimestamp: ์ŠคํŠธ๋ฆฌ๋ฐ ์„ธ์…˜ ์‹œ์ž‘ ํƒ€์ž„ ์Šคํƒฌํ”„
- songs: ๊ฐ ๋…ธ๋ž˜๋“ค์˜ duration์„ ๋‹ด์€ ๋ฐฐ์—ด []

rooms:${albumId}:session { releaseTimestamp "123123123"  songs "[97.812563,84.12]" }

song๋งˆ๋‹ค ์ €์žฅํ•˜๋˜ ๋ฐฉ์‹์—์„œ ํ•˜๋‚˜์˜ album์—์„œ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋…ธ๋ž˜๊ฐ€ ์กด์žฌํ•˜๊ณ , ๊ทธ ๋…ธ๋ž˜์˜ ํ”Œ๋ ˆ์ด ํƒ€์ž„์„ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, releaseTimestamp๋ฅผ ๋ฐ›์•„์™€์„œ, duration ์ •๋ณด๋ฅผ ๋ฐ˜๋ณต์„ ๋Œ๋ฆฌ๋ฉด์„œ ์–ด๋–ค ๋…ธ๋ž˜์˜ ์–ด๋””์ฏค ํ‹€์–ด์ค˜์•ผํ•˜๋Š”์ง€๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๋กœ์ง์„ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

์–ด๋–ค ๋…ธ๋ž˜๋Š” songIndex ๋กœ ๊ตฌ๋ถ„์„ ์ง€์—ˆ์Šต๋‹ˆ๋‹ค.

4. ๋ฒ„ํผ๋ง

์ถ”ํ›„ ๊ตฌํ˜„ ์˜ˆ์ •

inear

๊ธฐ์ˆ  ๊ณต์œ 

๐Ÿš€ ffmpeg๋Š” stderr๋กœ ๋””๋ฒ„๊น…์„ ํ•˜๋Š” ์ด์œ 
๐Ÿš€ HLS ํ”„๋กœํ† ์ฝœ์— ๊ด€ํ•œ ์ •๋ฆฌ ๋ฐ FFmpeg ์‚ฌ์šฉ๊ธฐ
๐Ÿš€ ๋น„ํŠธ๋Š” tsconfig.json์ด ์„ธ ๊ฐœ?
๐Ÿš€ NestJS ๊ธฐ๋ณธ ๊ฐœ๋… - Modules
๐Ÿš€ Socket.io ์ตœ(๊ฐ•)์ ํ™”
๐Ÿš€ ๋„์ปค์™€ nginx์˜ ์‚ฌ์šฉ๊ธฐ
๐Ÿš€ ๋ถ€ํ•˜ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ณด์ž

๊ฐœ๋ฐœ ์ผ์ง€

๐Ÿš€ FSD ์‚ฌ์šฉ๊ธฐ, ๊ทผ๋ฐ ์ด์ œ ๋‚˜๋งŒ์˜ ๊ทœ์น™์„ ๊ณ๋“ค์ธ
๐Ÿš€ CICD ๊ตฌ์กฐ ์ˆ˜์ •
๐Ÿš€ ์•จ๋ฒ” ๋‹จ์œ„๋กœ ์ŠคํŠธ๋ฆฌ๋ฐ ํ•˜๊ธฐ (with HLS)
๐Ÿš€ HLS๋กœ ์Œ์•… ์ฃผ๊ณ ๋ฐ›๊ธฐ
๐Ÿš€ vite + react + typescript ํ™˜๊ฒฝ์—์„œ path alias ์„ค์ •
๐Ÿš€ React Scan์ด ๋ญ์ฃ ?
๐Ÿš€ ๋กœ์ปฌ ํ™˜๊ฒฝ ๊ฐœ๋ฐœ ๋ชจ๋“œ ๋ฐฐํฌ
๐Ÿš€ ์•จ๋ฒ” ์ „์ฒด๋ฅผ ์ŠคํŠธ๋ฆฌ๋ฐํ•œ๋‹ค๊ณ ? (with HLS)
๐Ÿš€ ์ฝ”๋“œ์˜ ์•ˆ์ •์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž

ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

๐Ÿš€ ์ƒˆ๋กœ๊ณ ์นจ ์‹œ HLS ERROR
๐Ÿš€ input ํƒœ๊ทธ์— ํ•œ๊ธ€ ์ž…๋ ฅ ํ›„, Enter๋ฅผ ๋ˆ„๋ฅด๋ฉด ํ•จ์ˆ˜๊ฐ€ ๋‘๋ฒˆ ํ˜ธ์ถœ๋˜๋Š” ์˜ค๋ฅ˜
๐Ÿš€ nginx proxy pass๋ฅผ ๋ฐ”๊ฟจ๋”๋‹ˆ ์ƒ๊ธด ์—๋Ÿฌ - ์Šค์›จ๊ฑฐ ์ธ์‹ ๋ฌธ์ œ
๐Ÿš€ ๋ฐฐํฌ ํ™˜๊ฒฝ์—์„œ ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ WS handshake
๐Ÿš€ ๋ Œ๋”๋ง ๋ฒ”์ธ์€ ํ•˜๋‚˜!

ํ˜‘์—… ๊ทœ์น™

๐ŸŒˆ ๊ทธ๋ผ์šด๋“œ ๋ฃฐ
๐Ÿฅ” ํŒ€์› ์†Œ๊ฐœ
๐Ÿ”Ž ์ฝ”๋“œ & ๊นƒ ์ปจ๋ฒค์…˜
๐ŸŒณ ๊นƒ branch ์ „๋žต
๐Ÿ“Œ ๋…ธ์…˜ ๋ฌธ์„œ ์ €์žฅ์†Œ

ํ”„๋กœ์ ํŠธ ๊ธฐํš

๐ŸŽจ ํ”ผ๊ทธ๋งˆ
๐Ÿง‘โ€๐Ÿ’ป ๊ธฐํš ๊ณต์œ  ๋ฐœํ‘œ ์ž๋ฃŒ
๐ŸŽค 2์ฃผ์ฐจ ๋ฐœํ‘œ ์ž๋ฃŒ
๐Ÿ˜Ž ๋ฐฑ๋กœ๊ทธ

๋ฐ์ผ๋ฆฌ ์Šคํฌ๋Ÿผ

๐Ÿ“ 1์ฃผ์ฐจ
๐Ÿ“ 2์ฃผ์ฐจ
๐Ÿ“ 3์ฃผ์ฐจ
๐Ÿ“ 4์ฃผ์ฐจ
๐Ÿ“ 5์ฃผ์ฐจ

์ฃผ๊ฐ„ ๊ณ„ํš์„œ

๐Ÿ—“๏ธ 1์ฃผ์ฐจ
๐Ÿ—“๏ธ 2์ฃผ์ฐจ
๐Ÿ—“๏ธ 3์ฃผ์ฐจ
๐Ÿ—“๏ธ 4์ฃผ์ฐจ
๐Ÿ—“๏ธ 5์ฃผ์ฐจ

๊ทธ๋ฃน ํšŒ๊ณ 

โœจ 1์ฃผ์ฐจ
โœจ 2์ฃผ์ฐจ
โœจ 3์ฃผ์ฐจ
โœจ 4์ฃผ์ฐจ


view

Clone this wiki locally