영상 스트리밍 #7. 스트리밍 - HLS protocol

Bobby·2023년 3월 16일
1

streaming

목록 보기
7/9

이전 방식은 mp4 파일을 HTML5 video 태그를 이용하여 영상을 재생했다.

이번엔 스트리밍에 자주 사용되는 apple에서 개발한 HLS 프로토콜을 이용해 영상을 재생해보자.


🎥 HLS protocol

  • HTTP Live Streaming (HLS)는 Apple에서 개발한 스트리밍 프로토콜로, 인터넷에서 비디오를 스트리밍하기 위해 사용된다.
  • HLS는 HTTP기반의 스트리밍 프로토콜 이다.
  • HLS는 비디오를 작은 청크로 나누어 각 청크를 개별적으로 다운로드하고 재생한다.첫 번째 청크를 다운로드하고, 다음 청크를 다운로드하기 시작하기 전에 현재 재생 중인 비디오의 버퍼링을 계속 유지하여 지연 없이 실시간으로 비디오를 제공한다.
  • HLS를 재생하기위한 MIME 타입은 application/x-mpegURL 이다.
  • Apple에서 만든 프로토콜 이라서 safari 브라우저에서는 기본으로 동작한다.
  • HTML의 기본 video태그에서는 재생이 불가능 하다.
  • 크롬브라우저도 기본적으로 동작하지 않는다.

🎥 구성요소

m3u8

  • m3u8 파일은 HLS(HTTP Live Streaming) 방식으로 인코딩된 비디오나 오디오를 재생하기 위한 플레이리스트 파일이다.
  • 각 청크된 데이터 재생하기 위한 메타데이터가 저장되어있다.

ts

  • TS 파일은 MPEG transport stream 포맷으로 인코딩된 청크 파일이다.
  • TS 파일은 보통 2-10초 정도의 길이로 자르고, 비디오 및 오디오 데이터를 포함한다.
  • TS 파일은 m3u8 파일을 통해 클라이언트에게 제공, 클라이언트는 이 파일들을 다운로드하고 병합하여 비디오를 재생한다.

🎥 컨버팅

  • mp4파일을 hls(ts, m3u8) 형식으로 컨버팅 하자.
  • ffmpeg를 사용한다. 설치 ==> https://ffmpeg.org/
  • ffmpeg wrapper 사용
implementation 'net.bramp.ffmpeg:ffmpeg:0.7.0'

설정

FFmpegConfig

  • ffmpeg, ffprobe 를 빈으로 등록
@Slf4j
@Configuration
public class FFmpegConfig {
    @Value("${ffmpeg.path}")
    private String ffmpegPath;

    @Value("${ffprobe.path}")
    private String ffprobePath;

    @Bean
    public FFmpeg ffMpeg() throws IOException {
        return new FFmpeg(ffmpegPath);
    }

    @Bean
    public FFprobe ffProbe() throws IOException {
        return new FFprobe(ffprobePath);
    }
}

컨트롤러

ConvertController

@RestController
@RequiredArgsConstructor
public class ConvertController {

    private final ConvertService convertService;

    @ResponseBody
    @PostMapping("/convert/hls/{date}/{filename}")
    public String convertToHls(
            @PathVariable String date,
            @PathVariable String filename
    ) {
        convertService.convertToHls(date, filename);
        return "success";
    }
}

서비스

ConvertService

@Slf4j
@Service
@RequiredArgsConstructor
public class ConvertService {

    private final FFmpeg fFmpeg;
    private final FFprobe fFprobe;

    @Value("${tus.save.path}")
    private String savedPath;

    @Value("${tus.output.path.hls}")
    private String hlsOutputPath;

    @Value("${tus.output.path.mp4}")
    private String mp4OutputPath;

    public void convertToHls(String date, String filename) {
        String path = savedPath + "/" + date + "/" + filename;
        File output = new File(hlsOutputPath + "/" + filename.split("\\.")[0]);

        if (!output.exists()) {
            output.mkdirs();
        }

        FFmpegBuilder builder = new FFmpegBuilder()
                .setInput(path) // 입력 소스
                .overrideOutputFiles(true) 
                .addOutput(output.getAbsolutePath() + "/master.m3u8") // 출력 위치
                .setFormat("hls") 
                .addExtraArgs("-hls_time", "10") // 10초
                .addExtraArgs("-hls_list_size", "0")
                .addExtraArgs("-hls_segment_filename", output.getAbsolutePath() + "/master_%08d.ts") // 청크 파일 이름
                .done();

        run(builder);
    }

    private void run(FFmpegBuilder builder) {
        FFmpegExecutor executor = new FFmpegExecutor(fFmpeg, fFprobe);

        executor
                .createJob(builder, progress -> {
                    log.info("progress ==> {}", progress);
                    if (progress.status.equals(Progress.Status.END)) {
                        log.info("================================= JOB FINISHED =================================");
                    }
                })
                .run();
    }
}

실행

  • 업로드된 영상을 컨버팅 한다.

POST http://localhost:8080/convert/hls/{date}/{filename}

  • 포스트맨 사용하여 요청
  • 작업 완료
  • m3u8파일과 ts파일로 컨버팅 완료

🎥 재생

컨트롤러

HlsController

  • 컨텐츠 타입을 application/x-mpegURL 로 설정하여 내려준다.
  • m3u8의 정보를 읽어 다음 청크파일(ts)을 로딩한다.
@Controller
@RequiredArgsConstructor
public class HlsController {

    private final HlsService hlsService;

    @ResponseBody
    @RequestMapping("/hls/{key}/{filename}")
    public ResponseEntity<InputStreamResource> getHlsFile(
            @PathVariable String key,
            @PathVariable String filename
    ) throws FileNotFoundException {
        File file = hlsService.getHlsFile(key, filename);
        InputStreamResource resource = new InputStreamResource(new FileInputStream(file));
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType("application/x-mpegURL"))
                .body(resource);
    }

}

서비스

HlsService

@Service
@RequiredArgsConstructor
public class HlsService {

    @Value("${tus.output.path.hls}")
    private String outputPath;

    public File getHlsFile(String key, String filename) {
        return new File(outputPath + "/" + key + "/" + filename);
    }
}

사파리에서 재생

  • 사파리 브라우저는 hls를 지원하기 때문에 바로 재생이 가능하다.
  • 청크파일을 순차적으로 다운받아 재생한다.

http://localhost:8080/hls/dfb2effa54b843ec97e6dae71c845615/master.m3u8

video.js를 이용하여 재생

  • 기본 video 태그는 hls를 지원하지 않으므로 video.js 라이브러리를 사용하여 재생할 수 있다.

hls_player.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Uploaded vod</title>

    <link href="https://vjs.zencdn.net/7.14.3/video-js.css" rel="stylesheet" />
    <script src="https://vjs.zencdn.net/7.14.3/video.min.js"></script>
</head>
<body>
<video id="my-video" class="video-js" controls preload="auto" width="720" height="480">
    <source src="http://localhost:8080/hls/dfb2effa54b843ec97e6dae71c845615/master.m3u8" type="application/x-mpegURL">
</video>
<script>
    var player = videojs('my-video');
    player.play();
</script>
</body>
</html>

CORS 에러 발생시

CORS 설정

WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*");
    }
}
  • 청크파일을 순차적으로 다운받아 재생한다.


코드

profile
물흐르듯 개발하다 대박나기

1개의 댓글

comment-user-thumbnail
2024년 8월 27일

필요했던 내용입니다 감사합니다.

답글 달기