화질 별로 스트리밍 할 수 있도록 해보자.
다음 상황을 가정하고 테스트 한다.
tus protocol
사용하여 업로드한다.ffmpeg
를 사용한다.date
, filename
을 받는다.date
: 2023-03-24
filename
: f656c26134e147559b4d7d4c212a8d7b.mp4
ConvertControllerV2
@RestController
@RequiredArgsConstructor
public class ConvertControllerV2 {
private final ConvertServiceV2 convertServiceV2;
@PostMapping("/v2/convert/hls/{date}/{filename}")
public String convertToHls(
@PathVariable String date,
@PathVariable String filename
) {
convertServiceV2.convertToHls(date, filename);
return "success";
}
}
mp4
)을 FHD(1080p)
, HD(720p)
, SD(480p)
세개의 화질로 컨버팅(hls
) 한다.playlist.m3u8
파일을 참조하는 master.m3u8
파일을 생성한다.ConvertServiceV2
@Slf4j
@Service
@RequiredArgsConstructor
public class ConvertServiceV2 {
// FFmpeg, FFprobe 는 빈으로 등록해 두었다.
private final FFmpeg fFmpeg;
private final FFprobe fFprobe;
@Value("${tus.save.path}")
private String savedPath;
@Value("${tus.output.path.hls}")
private String hlsOutputPath;
public void convertToHls(String date, String filename) {
// 입력 파일 경로 설정
Path inputFilePath = Paths.get(savedPath + "/" + date + "/" + filename);
// 출력 폴더 경로 설정
Path outputFolderPath = Paths.get(hlsOutputPath + "/" + filename.split("\\.")[0]);
// 화질 별 폴더 생성
File prefix = outputFolderPath.toFile();
File _1080 = new File(prefix, "1080");
File _720 = new File(prefix, "720");
File _480 = new File(prefix, "480");
if (!_1080.exists()) _1080.mkdirs();
if (!_720.exists()) _720.mkdirs();
if (!_480.exists()) _480.mkdirs();
// ffmpeg 명령어 생성
FFmpegBuilder builder = new FFmpegBuilder()
.setInput(inputFilePath.toAbsolutePath().toString())
.addExtraArgs("-y")
.addOutput(outputFolderPath.toAbsolutePath() + "/%v/playlist.m3u8") // 출력 위치
.setFormat("hls")
.addExtraArgs("-hls_time", "10") // chunk 시간
.addExtraArgs("-hls_list_size", "0")
.addExtraArgs("-hls_segment_filename", outputFolderPath.toAbsolutePath() + "/%v/output_%03d.ts") // ts 파일 이름 (ex: output_000.ts)
.addExtraArgs("-master_pl_name", "master.m3u8") // 마스터 재생 파일
.addExtraArgs("-map", "0:v")
.addExtraArgs("-map", "0:v")
.addExtraArgs("-map", "0:v")
.addExtraArgs("-var_stream_map", "v:0,name:1080 v:1,name:720 v:2,name:480") // 출력 매핑
// 1080 화질 옵션
.addExtraArgs("-b:v:0", "5000k")
.addExtraArgs("-maxrate:v:0", "5000k")
.addExtraArgs("-bufsize:v:0", "10000k")
.addExtraArgs("-s:v:0", "1920x1080")
.addExtraArgs("-crf:v:0", "15")
.addExtraArgs("-b:a:0", "128k")
// 720 화질 옵션
.addExtraArgs("-b:v:1", "2500k")
.addExtraArgs("-maxrate:v:1", "2500k")
.addExtraArgs("-bufsize:v:1", "5000k")
.addExtraArgs("-s:v:1", "1280x720")
.addExtraArgs("-crf:v:1", "22")
.addExtraArgs("-b:a:1", "96k")
// 480 화질 옵션
.addExtraArgs("-b:v:2", "1000k")
.addExtraArgs("-maxrate:v:2", "1000k")
.addExtraArgs("-bufsize:v:2", "2000k")
.addExtraArgs("-s:v:2", "854x480")
.addExtraArgs("-crf:v:2", "28")
.addExtraArgs("-b:a:2", "64k")
.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();
}
}
%v
옵션값은 -var_stream_map
옵션에서 지정한 각 출력의 name
을 넣어준다. (name 을 설정하지 않으면 0, 1, 2, ...)master.m3u8
파일을 출력하기 위해서는 -hls_list_size
옵션을 넣어주어야 한다.POST http://localhost:8080/v2/convert/hls/2023-03-24/f656c26134e147559b4d7d4c212a8d7b.mp4
완료
각 화질별로 ts
, m3u8
파일이 생성 되었다.
각 playlist.m3u8 파일에는 ts 파일의 위치 정보가 저장 되어있다.
master.m3u8 파일에는 BANDWIDTH
에 따라 playlist.m3u8 을 지정해준다.
HlsControllerV2
@Controller
@RequiredArgsConstructor
public class HlsControllerV2 {
private final HlsServiceV2 hlsServiceV2;
// master.m3u8 경로
@ResponseBody
@RequestMapping("/v2/hls/{key}/{filename}")
public ResponseEntity<InputStreamResource> getMaster(
@PathVariable String key,
@PathVariable String filename
) throws FileNotFoundException {
File file = hlsServiceV2.getHlsFileV2(key, filename);
InputStreamResource resource = new InputStreamResource(new FileInputStream(file));
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/x-mpegURL"))
.body(resource);
}
// 각 화질별 ts, m3u8 경로
@ResponseBody
@RequestMapping("/v2/hls/{key}/{resolution}/{filename}")
public ResponseEntity<InputStreamResource> getPlaylist(
@PathVariable String key,
@PathVariable String resolution,
@PathVariable String filename
) throws FileNotFoundException {
File file = hlsServiceV2.getHlsFileV2(key, resolution, filename);
InputStreamResource resource = new InputStreamResource(new FileInputStream(file));
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/x-mpegURL"))
.body(resource);
}
}
@Service
@RequiredArgsConstructor
public class HlsServiceV2 {
@Value("${tus.output.path.hls}")
private String outputPath;
public File getHlsFileV2(String key, String resolution, String filename) {
return new File(outputPath + "/" + key + "/" + resolution + "/" + filename);
}
public File getHlsFileV2(String key, String filename) {
return new File(outputPath + "/" + key + "/" + filename);
}
}
hls
를 지원하는 사파리에서 실행 한다.http://localhost:8080/v2/hls/f656c26134e147559b4d7d4c212a8d7b/master.m3u8
다음과 같이 bandwidth 상황에 따라 화질을 변경하여 요청한다.
master.m3u8
요청
1080
의playlist.m3u8
요청
480
의playlist.m3u8
재요청
http://localhost:8080/v2/hls/f656c26134e147559b4d7d4c212a8d7b/720/playlist.m3u8
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/v2/hls/f656c26134e147559b4d7d4c212a8d7b/master.m3u8" type="application/x-mpegURL">
</video>
<script>
var player = videojs('my-video');
player.play();
</script>
</body>
</html>
배우고 갑니다