-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[NDD-361] 클라이언트 측에서 webm to mp4 인코딩 구현 (8h/8h) #195
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const convertTimeToSeconds = (timeStr: string) => { | ||
const [hours, minutes, seconds] = timeStr.split(':').map(Number); | ||
return hours * 3600 + minutes * 60 + seconds; | ||
}; | ||
|
||
const convertTimeToMinutes = (timeStr: string) => { | ||
const [minutes, seconds] = timeStr.split(':').map(Number); | ||
return minutes * 60 + seconds; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[p-5] dayJs에 요걸 처리해주는게 없으려나요..?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dayjs.extend(duration);
const convertTimeToSecondsWithDayjs = (timeStr) => {
const [hours = 0, minutes = 0, seconds = 0] = timeStr.split(':').map(Number);
return dayjs.duration({ hours, minutes, seconds }).asSeconds();
};
const convertTimeToMinutesWithDayjs = (timeStr) => {
const [minutes = 0, seconds = 0] = timeStr.split(':').map(Number);
return dayjs.duration({ minutes, seconds }).asMinutes();
};
gpt에서 물어본 결과인데 아무래도 이정도는 그냥 js로 처리해도 괜찮다 싶더라구요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
메모리 누수까지 해결되고 아주 완벽하군요!!
mp4 인코딩이 돼서 정말 다행입니다!
수고하셨습니다~~~~!
FE/src/utils/record.ts
Outdated
await ffmpeg.exec(['-i', 'input.webm', 'output.mp4']); | ||
const data = await ffmpeg.readFile('output.mp4'); | ||
const newBlob = new Blob([data], { type: 'video/mp4' }); | ||
toast.info('성공적으로 Mp4 인코딩이 성공했습니다😊'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[p-4] 성공했다는 말이 중복돼서 약간 어색한데 이렇게 바꾸는건 어떤가용??
toast.info('성공적으로 Mp4 인코딩이 성공했습니다😊'); | |
toast.info('성공적으로 Mp4 인코딩이 완료되었습니다😊'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영완료!
Deploying with Cloudflare Pages
|
Why
클라이언트 측에서 인코딩을 구현해야함에는 크게 두가지 이유가 있었습니다.
How
ffmpeg.wasm 공식 사이트
해당 사이트의 document를 성실히 따라갔습니다.
다만 차이를 둔 지점이 일부 존재합니다.
지금까지 ffmpeg에서 작성된 예제에 주석을 달아서 살펴보았습니다. 우리 코드에선 어떻게 구현되었는지 실제로 확인해봅니다.
FFmpeg 선언
Memory Leak 을 방지하기 위해 ffmpeg는 파일 로드시 최초 실행 후 다시 실행시키지 않습니다.
FFmpeg load
FFmpeg 를 실행시키기 위한 외부 모듈을 toBlobURL을 통해서 CORS 정책을 우회해서 받습니다.
이후 해당 로직은 라이브러리를 제외하고 함수로 전환이 가능할것으로 예상됩니다.
+) 본문에선 언급이 되지 않았지만, multi thread 를 지원하기 위해서 worker.js 모듈을 추가로 받아 사용합니다.
해당 모듈을 통해서 client에서 별도의 queue를 구현하지 않고 multi-thread로 인코딩이 진행됩니다.
사실 이과정 까지 진행되었다면 인코딩의 90퍼센트를 해결한것이나 다름없습니다.
input.webm 을 만들기 위한 파일데이터로 변경
해당 사진처럼 파일 데이터로 변경하기 위해 다음과 같이 file data를 변경합니다.
인코딩 진행
해당 사진에서처럼 별도로 인코딩을 진행합니다. 해당 로직이 종료 후 다시 blob 데이터로 변경해 반환합니다.
인코딩 과정 log 찍기
해당 사진에서 처럼 ffmpeg의 과정을 log 이벤트를 받아서 message를 출력할 수 있습니다.
다음 과정을 통해서 ffmpeg 과정중 toast 메세지를 통해서 진행상황을 반환할 수 있습니다.
Trouble Shooting
SharedArrayBuffer 보안 이슈 -> 해결
SharedArrayBuffer는 멀티스레딩 기능을 제공하는 고급 웹 API인데, 이는 Spectre와 같은 보안 취약점에 노출될 수 있습니다. 따라서, 최신 브라우저들은 이와 같은 API를 안전하게 사용하기 위해 COOP와 COEP 헤더 설정을 요구합니다.
Cross-Origin-Opener-Policy
same-origin 설정은 현재 페이지를 다른 출처의 페이지와 분리된 브라우징 컨텍스트 그룹에서 실행하도록 지시합니다.
same-origin 정책은 현재 페이지와 동일한 출처의 문서만이 현재 페이지와 같은 브라우징 컨텍스트 그룹에 속할 수 있음을 의미합니다.
이 설정은 특정 타입의 크로스-사이트 스크립팅 공격을 방지하는 데 도움이 됩니다.
Cross-Origin-Embedder-Policy
require-corp 설정은 웹 페이지가 모든 크로스 오리진 리소스에 대해 CORP (Cross-Origin Resource Policy) 헤더를 요구합니다.
require-corp (require a Cross-Origin Resource Policy) 정책은 페이지가 크로스 오리진 리소스를 로드하려면 해당 리소스가 명시적으로 크로스 오리진 사용을 허용해야 함을 의미합니다.
이 정책은 보안을 강화하며, SharedArrayBuffer와 같은 고급 API의 안전한 사용을 가능하게 합니다.
주의사항
이 헤더들을 설정하면 외부 리소스(예: CDN을 통해 제공되는 스크립트, 스타일시트, 이미지 등)의 로딩에 영향을 줄 수 있습니다. 따라서, 이러한 리소스가 CORP 헤더를 적절히 설정하고 있는지 확인해야 합니다.
Critical dependency: the request of a dependency is an expression -> 완벽한 해결은 못함, Pending
Critical dependency: the request of a dependency is an expression 경고는 Webpack이 모듈을 동적으로 로드하는 코드를 만났을 때 발생합니다. 이 경우, @ffmpeg/ffmpeg 라이브러리의 worker.js 파일 내에서 발생하는 것으로 보입니다. Webpack은 기본적으로 동적으로 해석되는 의존성(예: 변수를 사용하는 require 호출)을 처리하는 데 어려움을 겪습니다.
다음과 같은 구문을 포함해서 해결 하려 했습니다.
하지만 해당 방식으로는 ffmpeg 의 worker.js의 경로를 찾지 못하는 이슈가 발생하여, 당장의 에러는 발생시키지 않지만, "동작하지 않는" 문제가 발생합니다. 그래서 해당 방식은 바로 폐기하고 진행했습니다.
그래서 다음과 같이 해결했습니다. 해당 문제는 동적으로 생성되는 worker.js를 찾지 못할 수도 있다는 webpack의 경고로 인해서 시작합니다. 하지만 해당 파일은 내부의 toBlobUrl 을 통해서 import 된 모듈들로 인해 동적으로 잘 연결되고 있기에 현재로선 이 과정을 통해서 빌드 프로세싱 중에서 문제가 발생하지 않도록 합니다.
이 경우 더 이상의 문제는 발생하지 않습니다.
Memory leak 이슈 -> 해결
최초의 코드였습니다. 해당 코드는 함수가 실행될때마다 ffmpeg를 새롭게 정의해서 받아옵니다.
하지만 이 경우 해당 함수를 여러번 실행할 때마다, 새로운 ffmpeg를 받기 때문에 ffmpeg.loaded가 동작하지 않고 매번 새로운 모듈을 받게 됩니다.
이를 해결하기 위해서 다음과 같이 선언부에서 ffmpeg 객체를 선언합니다.
아래는 그 결과입니다.
더 이상 여러번 호출하더라도 메모리가 Leak 되지 않음을 확인하였습니다.
Result
mp4로 출력됨을 확인할 수 있습니다.2023-12-12.12.13.11.mov
To Reviewer
PR이 짧지만 많은 내용이 들어가 있습니다. 해당 로직에서 여러 에러처리들도 겸해서 함께 진행되어 있으니, 함께 봐주시면 좋을것 같습니다!
모두들 즐거운 하루 되시길 😊