-
Notifications
You must be signed in to change notification settings - Fork 1
Cloud Functions로 음악 인코딩하기(성공편)
우리의 현재 로직은 업로드 될 때마다 음악이 인코딩 되면 쓰이지 않는 파일(ts, m3u8)이 많이 발생될까 염려되어 음악 정보를 DB에 업로드 할 때 인코딩 하는 방식을 선택했다.
그러나 이렇게 되면 인코딩 하는데 시간이 3초~20초 안쪽까지도 넘어가는데 사용자는 이 과정이 완료될 때까지 제출 버튼만 누르고 기다려야 한다는 점이다!
그러던 찰나에, 채리님께서 custom image 활용하여 Cloud Functions에서 인코딩에 성공하셨다는 큰 한마디를 전해듣고..! 잘 모르던 docker도 봐볼겸 다시 도전해보기로 했다! 이렇게 되면 업로드할 때 클라우드에서 따로 인코딩이 돌아가기 때문에, 서버의 부담도 줄고 사용자가 기다릴 필요도 없음!)
- 버킷의 특정 경로(
/music
하위)에 음악(.mp3
)이 업로드 되면 trigger 시킨다. - ffmpeg, aws-sdk 등 인코딩에 필요한 모듈이 설치된 docker image에서 인코딩을 실시한다.
-
params
로 파일 정보가 넘어옴. - 받아온 파일(mp3)을 특정 경로에 담아 저장
- ffmpeg을 통해 이 경로안의 파일을 인코딩 → 특정 경로에 저장 → 바로 Object Storage에 업로드!
-
- 변환 끝-!
(ncloud 공식 문서를 보면 어떻게 파일을 작성해야 하는지 친절히 알려준다!)
사실 docker를 설치하지 않아서 docker를 설치하는 과정이 제일 오래 걸렸다. wsl의 버전을 최신으로 업데이트 해야 docker가 실행이 되어..
Dockerfile
FROM cloudfunctions.kr.ncr.ntruss.com/cloudfunctions-nodejs-16:latest
RUN npm install fluent-ffmpeg
RUN npm install @ffmpeg-installer/ffmpeg
RUN npm install axios
RUN npm install aws-sdk
WORKDIR /nodejsAction
CMD node --expose-gc app.js
powershell 명령어
# 아래의 로직을 따라 container에 push까지 해주면 이 이미지를 cloud functions에서 가져와주면 됨!
docker build . -t [container registry 이름].kr.ncr.ntruss.com/[image:tag]
docker login -u {NCP access key} catchy-tape-encode-registry.kr.ncr.ntruss.com
password: {NCP secret key}
docker push [container registry 이름].kr.ncr.ntruss.com/[image:tag]
-
코드가 보고 싶다면?
const AWS = require('aws-sdk'); const ffmpeg = require('fluent-ffmpeg'); const { path } = require('@ffmpeg-installer/ffmpeg'); const fs = require('fs'); const axios = require('axios'); const Path = require('path'); function setObjectStorage(accessKey, secretKey) { return new AWS.S3({ endpoint: 'https://kr.object.ncloudstorage.com', region: 'kr-standard', credentials: { accessKeyId: accessKey, secretAccessKey: secretKey, }, signatureVersion: 'v4', }); } function separateMusicName(musicPath) { const parsedPath = new URL(musicPath); const pathNames = parsedPath.pathname.split('/'); const musicName = pathNames[pathNames.length - 1]; return musicName; } function getPath(option) { return Path.resolve(`musics${option}`); } function setEncodingPaths(musicPath) { const musicName = separateMusicName(musicPath); return { outputMusicPath: getPath('/output'), entireMusicPath: getPath(''), outputPath: getPath(`/output/${musicName.replace('.mp3', '')}.m3u8`), tempFilePath: getPath(`/${musicName}`), }; } async function uploadToObjectStorage( filePath, musicId, fileName, objectStorage, ) { const result = await objectStorage .upload({ Bucket: 'catchy-tape-bucket2', Key: `music/${musicId}/${fileName}`, Body: fs.createReadStream(filePath), ACL: 'public-read', }) .promise(); return { url: result.Location }; } async function uploadEncodedFile(filePath, musicId, fileName, objectStorage) { try { const { url } = await uploadToObjectStorage( filePath, musicId, fileName, objectStorage, ); return url; } catch (err) { console.log(err); } } async function saveMp3File(outputMusicPath, musicPath, tempFilePath) { fs.mkdirSync(outputMusicPath, { recursive: true }); const musicFileResponse = await axios.get(musicPath, { responseType: 'arraybuffer', }); const musicBuffer = Buffer.from(musicFileResponse.data); fs.writeFile(tempFilePath, musicBuffer, (err) => { if (err) throw new Error(); }); } async function encodeMusic(musicId, musicPath, objectStorage) { try { ffmpeg.setFfmpegPath(path); const { outputMusicPath, outputPath, tempFilePath } = setEncodingPaths(musicPath); await saveMp3File(outputMusicPath, musicPath, tempFilePath); const encodedFileURL = await executeEncoding( tempFilePath, outputPath, outputMusicPath, musicId, objectStorage, ); return encodedFileURL; } catch (err) { console.log(err); } } async function executeEncoding( tempFilePath, outputPath, outputMusicPath, musicId, objectStorage, ) { let m3u8FileName = ''; let m3u8Path = ''; const watcher = fs.watch(outputMusicPath, async (eventType, fileName) => { if (fileName.match(/.m3u8$/)) { m3u8FileName = fileName; } else if (!fileName.match(/\.tmp$/)) { await uploadEncodedFile( outputMusicPath + `/${fileName}`, musicId, fileName, objectStorage, ); } }); return await new Promise((resolve, reject) => { ffmpeg(tempFilePath) .addOption([ '-map 0:a', '-c:a aac', '-b:a 192k', '-hls_time 30', '-hls_list_size 0', '-f hls', ]) .output(outputPath) .on('end', async () => { watcher.close(); m3u8Path = await uploadEncodedFile( outputMusicPath + `/${m3u8FileName}`, musicId, m3u8FileName, objectStorage, ); resolve(m3u8Path); }) .on('error', () => { reject(new Error()); }) .run(); }); } async function main(params) { const bucket = params.container_name; const music_id = params.music_id; const musicPath = `https://kr.object.ncloudstorage.com/${bucket}/music/${music_id}/music.mp3`; const objectStorage = setObjectStorage(params.accessKey, params.secretKey); const url = await encodeMusic(music_id, musicPath, objectStorage); return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: { url, }, }; }
(multipart/form-data 형태로 파일이 업로드 되어 이벤트 유형을 아래와 같이 설정했다)
- 테스트: 3분 56초 음악 4.8s (기존에 인코딩을 개선했을 때 5초대까지가 최선이었음)
이렇게 mp3 파일이 버킷에 업로드될 때 트리거가 발생하여 알아서 저장되면, 서버 입장에서는 언제 인코딩이 끝나는지 알 수 없었다. 그래서 웹 액션으로 방식을 바꿔주었다!
- API Gateway를 통해 웹 액션을 호출할 수 있는 외부 연결 주소 생성하기
(위의 그림처럼 직접 요청을 보냈을 때 인코딩 성공 후 응답이 오는 것을 확인!)
-
웹 액션에도 동일하게 custom image와 app.js 파일을 활용해 소스 코드 설정하기
-
음악 버킷에 업로드 요청 → 버킷에 업로드 → 1번의 URL에 POST 요청 → 인코딩 완료! → 응답
결론적으로는 위의 과정을 통해 서버로 응답이 돌아온다!
위 페이지의 ‘외부 연결 주소 생성’ 목차를 통해 URL의 의미를 파악했고,
위 페이지의 ‘IAM 인증이 필요한 API 호출’ 목차를 통해 필요한 인증 헤더를 넣어주었다.
↓
성공 ~! ~!
그러나 서버에서 인코딩 에러가 가끔씩 발생한다. 이건 (아직까지 생각하기에는) 간헐적으로 발생하는 에러라고 추측됨. (일단 NCP 탓)
- 프로젝트 생성
- 프로젝트 구조
- PR에 대한 단위 테스트 자동화
- 역/직렬화 라이브러리 비교
- Github Release 자동화
- Firebase App 배포 자동화
- 플러그인을 이용하여 공통 설정 없애기
- Timber 라이브러리를 사용한 이유
- 네트워크 예외 처리
- Kotest 도입기