학교에서 진행하는 프로젝트에서 비디오를 조회하는 기능을 넣다 보니 자연스레 썸네일을 어떻게 다룰지에 대해 생각해 보게 되었다.
먼저 개발 중인 환경은 다음과 같다.
- Google Cloud Platform 사용
- Java 17, Spring Boot 3.2.4
- next.js 14.2.3
현재 비디오는 Google Cloud Storage(AWS S3에 해당)에 저장되어 공개 URI 형태로 DB에 저장된다.
Youtube처럼 비디오 검색을 했을때 각 비디오 정보와 함께 비디오 재생 전에 썸네일을 표시하고 싶었다.
썸네일은 사용자 지정 썸네일이라기보다는, 특정 타임스탬프의 프레임을 쓰기로 했다.
총 세가지 경우를 생각했다.
1. Spring 애플리케이션에서 처리
- 백엔드 인스턴스 사양이 그리 좋지 못하고, ffmpeg도 인스턴스 내에 따로 설치 (정확히는 docker image 안에 설치)해야하고 , 작동도 원활하게 되지 않아 PASS
2. 프론트엔드 단에서 처리
- 동영상이 몇 분 ~ 몇십 분 짜리다 보니 프론트에서 썸네일을 검색된 시점에 실시간으로 처리하려니 수 초의 로딩 시간이 걸림. 따라서 PASS
3. Cloud Functions를 사용해서 Cloud Storage에 영상이 올라오는 시점에서 처리
- Cloud Storage에 영상이 올라오는 시점을 감지할 수 있는 Trigger가 있어 업로드 될 때 즉각적으로 처리 가능, 서버리스 함수이기 때문에 인스턴스 사양과 관계 없음
Cloud Functions 알아보기
Cloud Functions는 AWS Lambda와 비슷한 서버리스 실행 환경이다. Cloud Functions를 사용하면 클라우드 인프라와 서비스에서 발생하는 이벤트에 대한 함수를 작성할 수 있다.
이벤트 트리거
Cloud 이벤트란 클라우드 환경에서 발생하는 모든 상황을 의미한다. 예를 들어 데이터베이스 변경, 저장소(Cloud Storage)에 파일 추가, VM 생성 등이 있다. 경우에 따라서는 HTTP 요청도 가능.
이벤트는 응답 여부에 관계없이 발생하고, 이러한 이벤트 트리거를 이용하면 특정 클라우드 이벤트에 대하여 작업을 수행할 수 있다.
트리거는 두 가지 카테고리로 분류됨
- HTTP 트리거: 일종의 REST API 역할로 사용됨
- 이벤트 트리거: GCP 내의 이벤트를 감지하여 특정 작업 수행
Cloud Storage 트리거
Cloud Storage의 이벤트를 감지하여 Cloud Functions를 호출하는 트리거
- 새로운 파일 업로드시
google.cloud.storage.object.v1.finalized
- 파일이 삭제되었을 때
google.cloud.storage.object.v1.deleted
requirements.txt
ffmpeg-python
requests
google-cloud-storage
functions-framework
cloudevents
main.py
import os
import tempfile
import requests
from google.cloud import storage
import ffmpeg
import functions_framework
from cloudevents.http import CloudEvent
def extract_thumbnail(video_path, thumbnail_path):
try:
(
ffmpeg
.input(video_path, ss=1)
.output(thumbnail_path, vframes=1)
.run(capture_stdout=True, capture_stderr=True)
)
except ffmpeg.Error as e:
print(f"ffmpeg error: {e.stderr.decode('utf-8')}")
raise
@functions_framework.cloud_event
def thumbnail_extractor(cloud_event: CloudEvent):
try:
file_data = cloud_event.data
bucket_name = file_data['bucket']
file_name = file_data['name']
if not file_name.endswith(('.mp4', '.mov', '.avi', '.mkv')):
print(f"Skipping non-video file: {file_name}")
return
storage_client = storage.Client()
bucket = storage_client.bucket(bucket_name)
blob = bucket.blob(file_name)
with tempfile.NamedTemporaryFile(delete=False) as temp_video:
blob.download_to_filename(temp_video.name)
video_path = temp_video.name
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_thumbnail:
thumbnail_path = temp_thumbnail.name
extract_thumbnail(video_path, thumbnail_path)
thumbnail_blob = bucket.blob(f'thumbnail/{os.path.basename(thumbnail_path)}')
thumbnail_blob.upload_from_filename(thumbnail_path)
video_url = f"https://storage.googleapis.com/{bucket_name}/{file_name}"
thumbnail_url = thumbnail_blob.public_url
data = {
"title": file_name,
"description": file_name,
"thumbnailUri": thumbnail_url,
"videoUri": video_url,
"city": "서울",
"district": "강남구"
}
update_db_api(data)
os.remove(video_path)
os.remove(thumbnail_path)
except Exception as e:
print(f"Error processing file {file_name}: {e}")
def update_db_api(data):
try:
api_url = 'https://api.a-eye.live/video'
response = requests.post(api_url, json=data)
if response.status_code == 200:
print('Database updated successfully')
else:
print(f'Failed to update the database: {response.status_code}, {response.text}')
except Exception as e:
print(f"Error updating database: {e}")
썸네일을 추출한 다음, 우리 웹 서비스 API로 POST /video 요청을 날리면 비디오 정보가 DB에 저장되는 방식이다. 이를 통해 서로 연결되지 않은 Cloud Storage와 RDBMS간에 비디오 업로드 시 자동으로 RDBMS도 연동이 된다.
'개발 > Cloud' 카테고리의 다른 글
[Cloud] AWS S3 파일 버전 관리 및 정책 설정하기 (4) | 2024.09.26 |
---|---|
[Cloud] AWS S3로 정적 웹 사이트 호스팅하기 (1) | 2024.09.26 |
[Cloud] 구름 IDE를 처음 사용한다면 (2) | 2022.10.23 |