개발놀이터

스프링으로 영상 랜더링하기 (Resource Range) 본문

Spring/Spring

스프링으로 영상 랜더링하기 (Resource Range)

마늘냄새폴폴 2024. 6. 14. 20:40

넷플릭스나 유튜브를 보면 이런 경험 있으실겁니다. 

 

 

이렇게 회색바가 앞으로 천천히 진행하는 것을말이죠. 

 

이렇게 하는 이유는 영상의 크기가 어마어마하다보니 이걸 전부 랜더링하고 사용자에게 내려주면 사용자가 꽤나 오랜시간 기다려야하기 때문에 먼저 영상을 내려주고 조금씩 랜더링하는 방식을 사용합니다. 

 

저도 이걸 구현하게 될 줄은 몰랐습니다. 하지만 많은 레퍼런스가 있어서 따라하기 편했고 의외로 쉽게 구현할 수 있었습니다. 

 

이번 포스팅에선 스프링으로 영상 랜더링하고 리액트로 응용하는 것까지 한번 정리하고 공유해보도록 하겠습니다. 

 

스프링으로 영상 랜더링하기

@RestController
@RequiredArgsConstructor
@Slf4j
public class CouponController {

    @Value("${record-path}")
    String recordPath;
    @GetMapping("/api/video/{streamId}")
    public ResponseEntity<ResourceRegion> videoRegion(@RequestHeader HttpHeaders headers, @PathVariable("streamId") String streamId) throws Exception {
        String path = recordPath + streamId + ".webm"
        Resource resource = new FileSystemResource(path);

        long chunkSize = 1024 * 1024 * 10;	// 10MB
        long contentLength = resource.contentLength();

        ResourceRegion region;

        try {
            HttpRange httpRange = headers.getRange().stream().findFirst().get();
            long start = httpRange.getRangeStart(contentLength);
            long end = httpRange.getRangeEnd(contentLength);
            long rangeLength = Long.min(chunkSize, end -start + 1);

            log.info("start === {} , end == {}", start, end);

            region = new ResourceRegion(resource, start, rangeLength);
        } catch (Exception e) {
            long rangeLength = Long.min(chunkSize, contentLength);
            region = new ResourceRegion(resource, 0, rangeLength);
        }

        return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
                .cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES))
                .contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
                .header("Accept-Ranges", "bytes")
                .eTag(path)
                .body(region);
    }
 }

 

이게 끝입니다! 여기서 중요한건 chunkSize만큼 추가 랜더링을 진행한다는 점입니다. 즉, 저 회색바를 얼마만큼으로 할지를 chunkSize가 결정한다고 보시면 됩니다. 

 

저희 회사 서비스는 사용자가 선택함에따라 영상이 바뀌어야해서 리액트쪽에서 조금 이 랜더링을 활용했습니다. 

 

function Video(props) {
	const {streamId, startTime} = props
    
    useEffect(() => {
    	video.current.currentTime = startTime;
    }, [startTime])
    
    return (
    	<video autoPlay controls ref={video} key={streamId}>
        	<source src={'api/video/' + streamId} type="video/webm">
        </video>
    )
}

 

일단 이렇게 Video 컴포넌트를 만들어줍니다. 그리고 이걸 사용하는 쪽에선

 

function StudentView() {
	const [nowVideo, setNowVideo] = useState({
    	streamId: '초기값',
        startTime: 0
    });
    const [videoInfo, setVideoInfo] = useState({});
    
    axios.get('/api/video', headers: {"Content-Type": "application/json"})
    	.then((response) => {
        	setVideoInfo(response);
        });
    
    const setNowVideo(streamId, startTime) = () => {
    	setNowVideo({...nowVideo, streamId: streamId, startTime: startTime});
    }

	return (
    	<div>
        	{videoInfo.map((video) => {
            	<button onClick={() => setNowVideo(video.streamId, video.startTime)}>선택구간</button>
            })}
        	<Video streamId={nowVideo.streamId} startTime={nowVideo.startTime}/>
        </div>
    )
}

 

이렇게 선택구간에 시작시간을 서버에서 가져온 뒤 "선택구간"이라는 버튼을 클릭하면 nowVideo값을 바꿔주면서 <video> 태그를 리랜더링하는 방식입니다. 

 

제가 리액트를 사용한지 얼마 안되서 잘 몰랐던게 리액트에선 key라는 파라미터로 리랜더링 여부를 결정하는 것 같더군요. 음... 아닐수도 있습니다. 

 

그래서 처음엔 <video> 태그 안에 있는 src만 상태값을 변경했더니 src는 정상적으로 변경됐지만 비디오가 랜더링이 안되서 처음에 재생했던 영상을 계속 재생했습니다. 

 

대신 key라는 파라미터는 말 그대로 PK같은 역할을 해야하더군요. map으로 여러개 두어도 key가 달라야한다고 사수가 말해줬습니다. 

 

그래서 절대 중복될리 없는 streamId를 key로 주어 streamId가 바뀔때마다 <video>태그가 리랜더링되면서 사용자에게 보여줄 수 있었습니다. 

 

 

마치며

회사 서비스에 직접적인 기여를 한건 이번이 처음이네요. 사실 이건 버전2고 버전1이 따로 있습니다. 지금 대표님의 컨펌을 기다리고 있죠.

 

버전1은 버튼을 클릭하면 모달로 나와서 영상을 크게 보여주는 것입니다. 이렇게 개발했는데 상무님이 현재 페이지에서 모달말고 화면공유가 되는 곳을 화면공유 말고 녹화해놓은 영상으로 보여주는게 어떻겠냐고 하셔서 버전2를 만들게 되었습니다. 

 

모달까지 포스팅하면 스프링으로 영상 랜더링이 아니고 리액트로 비디오태그 조절하기가 될 것같아서 빼놓았습니다. 

 

회사에서 리액트를 사용하고 OpenVidu도 리액트로 개발해놔서 요즘 리액트 코드를 많이 보고 많이 접하고 많이 개발하게 되었습니다. 

 

덕분에 손에 조금 익은 것 같은데 아직 최적화라던가 이런 부분이 조금 약한 것 같습니다. 

 

백엔드 개발자가 백엔드만 알면 그건 미덕이 못된다고 생각하는 철학을 가지고 있는 저로서 회사에서 프론트에 대해서 공부하면서 서비스에 기여까지 해보고 오래살고볼 일입니다. 백엔드에만 투자해온 저에게 황금같은 시간이었습니다. 

 

이번 포스팅은 여기까지입니다! 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~