-Case study of building a video manual service based on ReactPlayer-
1. Project Overview
This case is a project carried out to transition the existing PDF-based user manual system to a video content-based service. In the existing system, when users clicked the manual button, the frontend directly accessed AWS S3 to download the PDF file. Although this method was simple to implement, it had limitations in that users found it difficult to simultaneously view the manual and their actual work screen, making it challenging to intuitively convey complex procedures.
The client required the PIP (Picture in Picture) feature so that users could view the manual video while maintaining their work screen. At the same time, due to security policies, direct access to AWS S3 had to be blocked, and there were constraints that sensitive authentication information could not be included in the URL parameters. Therefore, the core of this project was not merely implementing a simple video playback feature, but designing an authentication and streaming structure that satisfied both browser-based video playback architecture and security requirements.
2. Technology Selection and Design Direction
In the Frontend area, we introduced ReactPlayer, a React-based video player library. ReactPlayer operates on the HTML5 video tag, boasts high compatibility with the React environment, and offers the advantages of a relatively simple PIP feature and playback state control.
However, ReactPlayer uses the video tag's request method internally, so video file requests are primarily performed using the GET method. Since it's difficult to include authentication information in the request body or freely configure the headers like a typical API call, it was necessary to design a separation between the authentication process and the actual video playback request.
Sample Code 1. Example of calling ReactPlayer
<ReactPlayer
url={videoEndpoint}
controls
pip
playing={false}
width="100%"
height="100%"
/>
The above example shows the structure of how ReactPlayer plays a video based on the video playback endpoint received from the server. The authentication information is not included in the URL; instead, it is handled by passing a temporary token issued during the pre-authentication phase as a Cookie.
3. Architecture Redesign
The existing structure allowed the Frontend to directly access AWS S3. However, maintaining the same structure when providing video content could lead to exposure of storage paths, URL reuse, and potential circumvention of access controls. Accordingly, we have separated the server roles as follows.
Frontend
-> Server A (사용자 인증 및 스트리밍 요청 중계)
-> Server B (파일 조회 전용 서버)
-> AWS S3
Server A handles user authentication, permission verification, issuing temporary tokens, and streaming request mediation. Server B has been separated to only handle actual video file retrieval and AWS S3 access. This prevents clients from directly knowing the storage paths and ensures that all video requests pass the server's authentication verification.
4. Authentication and Playback Flow
When the user clicks the manual button, it does not immediately call the video player, but first requests the user verification API from Server A. Once the verification is complete, Server A issues an endpoint for video playback and a time-limited temporary token. The temporary token is stored in the browser's Cookie and is subsequently passed along when ReactPlayer calls the video endpoint.
Sample Code 2. Requesting Video Playback Information
const openManualVideo = async (manualId: string) => {
const response = await api.get(`/api/videos/${manualId}/authorize`);
setVideoEndpoint(response.data.videoEndpoint);
setManualModalOpen(true);
};
The above code is an example that first requests the video playback endpoint when the manual button is clicked. At this stage, the server responds by validating the user's permissions and storing a temporary token in the Cookie.
Sample Code 3. Example of Issuing Temporary Token Cookie
ResponseCookie cookie = ResponseCookie.from("VIDEO_ACCESS_TOKEN", token)
.httpOnly(true)
.secure(true)
.path("/api/videos/")
.maxAge(Duration.ofMinutes(30))
.sameSite("Strict")
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(new VideoAuthorizeResponse(videoEndpoint));
The Cookie has applied HttpOnly, Secure, and SameSite options to restrict script access and unnecessary external transmissions. It is also designed to be used restrictively for video requests by specifying the path and maxAge.
5. Streaming Relay Processing
Video streaming continuously delivers large amounts of data, unlike regular file downloads, and when users move the playback position, range header-based partial requests may occur. Therefore, Server A must forward the client's range header to Server B and relay the response status and key headers from Server B back to the client.
Sample Code 4. Streaming Relay Based on Range Header
@GetMapping("/api/videos/{manualId}/stream")
public Mono>> stream(
@PathVariable String manualId,
@RequestHeader(value = HttpHeaders.RANGE, required = false) String range
) {
return webClient.get()
.uri("/internal/files/{manualId}", manualId)
.headers(headers -> {
if (range != null) headers.set(HttpHeaders.RANGE, range);
})
.exchangeToMono(response -> {
Flux body = response.bodyToFlux(DataBuffer.class);
ResponseEntity.BodyBuilder builder = ResponseEntity.status(response.statusCode());
response.headers().asHttpHeaders().forEach((name, values) -> {
if (isStreamingHeader(name)) builder.header(name, values.toArray(String[]::new));
});
return Mono.just(builder.body(body));
});
}
The key to this method is not loading the entire file into memory like bodyToMono(Resource.class) or bodyToMono(byte[].class). By using bodyToFlux(DataBuffer.class), video data can be transmitted in chunks, making it suitable for large-scale video processing.
Sample Code 5. Main Streaming Header Filtering
private boolean isStreamingHeader(String name) {
return HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)
|| HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)
|| HttpHeaders.CONTENT_RANGE.equalsIgnoreCase(name)
|| HttpHeaders.ACCEPT_RANGES.equalsIgnoreCase(name)
|| HttpHeaders.CACHE_CONTROL.equalsIgnoreCase(name);
}
Headers such as Content-Range, Accept-Ranges, and Content-Length are necessary for the browser to determine the length of the video and the part response range. It is especially important to preserve these headers for functionality like seeking and stable playback.
6. Results and Effects
As a result of the implementation, users were able to play videos immediately in the browser and simultaneously utilize the actual work screen and video manual through the PIP feature. Additionally, by removing direct access to AWS S3 and applying server-based access control, security requirements were met.
• Remove direct exposure of AWS S3
• Remove transmission of sensitive information based on URL
• Allow access to video endpoint after user validation
• Temporary token-based access control
• Support for video playback and navigation based on Range Header
7. Conclusion
Through this case, I experienced a design case that comprehensively considered not just the simple task of adding a video player, but also the operation of ReactPlayer and HTML5 video tags, the client's security policies, the separation of roles between servers, and streaming processing methods. By separating the authentication and playback processes and clearly delineating the responsibilities of Server A and Server B, we were able to ensure both user experience and security.
In particular, I felt that in a practical environment, it is important to understand the constraints of security policies and browser operations alongside the implementation of features themselves. This project confirmed that even in video-based manual services, considering the authentication structure, data flow, streaming header processing, and operational security policies is essential for providing a stable service.
NZ