흥미진진한 SaaS 구축 여정을 시작한 지 4년이 지났으니 이제 앱의 핵심 구성 요소 중 하나를 다시 빌드해야 할 때입니다.
JavaScript로 작성된 소셜 미디어 동영상을 위한 간단한 동영상 편집기.
이 재작성에 사용하기로 결정한 스택은 다음과 같습니다. 현재 작업이 진행 중입니다.
우리 프런트엔드는 SvelteKit으로 작성되었으므로 이것이 우리 사용 사례에 가장 적합한 옵션입니다.
동영상 편집기는 프런트엔드에 간단히 추가할 수 있는 별도의 비공개 npm 라이브러리입니다. 헤드리스 라이브러리이므로 영상 편집기 UI가 완전히 분리되어 있습니다.
비디오 편집기 라이브러리는 비디오 및 오디오 요소를 타임라인과 동기화하고, 애니메이션 및 전환을 렌더링하고, HTML 텍스트를 캔버스로 렌더링하는 등의 작업을 담당합니다.
SceneBuilderFactory는 장면 JSON 개체를 인수로 사용하여 장면을 생성합니다. StateManager.svelte.ts는 실시간으로 비디오 편집기의 현재 상태를 유지합니다.
이 기능은 타임라인에서 플레이헤드 위치를 그리고 업데이트하는 등의 작업에 매우 유용합니다.
Pixi.js는 뛰어난 JavaScript 캔버스 라이브러리입니다.
처음에는 Pixi v8을 사용하여 이 프로젝트를 구축하기 시작했지만 이 기사 뒷부분에서 언급할 몇 가지 이유로 인해 Pixi v7을 사용하기로 결정했습니다.
그러나 비디오 편집기 라이브러리는 종속성과 밀접하게 결합되어 있지 않으므로 필요한 경우 쉽게 교체하거나 다른 도구를 테스트할 수 있습니다.
타임라인 관리와 복잡한 애니메이션에는 GSAP를 사용하기로 결정했습니다.
내가 아는 JavaScript 생태계에는 이렇게 간단한 방법으로 중첩된 타임라인, 결합된 애니메이션 또는 복잡한 텍스트 애니메이션을 구축할 수 있는 다른 도구가 없습니다.
GSAP 비즈니스 라이센스가 있으므로 추가 도구를 활용하여 더 많은 작업을 단순화할 수도 있습니다.
백엔드에서 사용하는 기능을 살펴보기 전에 자바스크립트로 비디오 편집기를 구축하는 동안 해결해야 할 몇 가지 과제를 살펴보겠습니다.
GSAP 포럼에서 자주 묻는 질문입니다.
타임라인 관리를 위해 GSAP를 사용하는지 여부는 중요하지 않습니다. 해야 할 일은 몇 가지입니다.
각 렌더링 틱에서:
타임라인에 대한 비디오 상대 시간을 가져옵니다. 타임라인의 10초 지점에서 동영상이 처음부터 재생되기 시작한다고 가정해 보겠습니다.
음, 10초 이전에는 실제로 동영상 요소에 관심이 없지만 타임라인에 들어가자마자 동기화를 유지해야 합니다.
비디오 요소의 currentTime에서 계산해야 하는 비디오의 상대적 시간을 계산하여 현재 장면 시간과 허용 가능한 "지연" 기간 내 비교하여 이를 수행할 수 있습니다.
지연 시간이 0.3초보다 클 경우 비디오 요소를 자동으로 검색하여 기본 타임라인과의 동기화를 수정해야 합니다. 이는 오디오 요소에도 적용됩니다.
기타 고려해야 할 사항:
재생 및 일시정지는 구현이 간단합니다. 검색을 위해 비디오 검색 구성 요소 ID를 svelte StateManager에 추가합니다. 그러면 자동으로 상태가 "로드 중"으로 변경됩니다.
StateManager에는 EventManager 종속성이 있으며 각 상태 변경 시 자동으로 "changestate" 이벤트를 트리거하므로 $효과를 사용하지 않고도 이러한 이벤트를 수신할 수 있습니다.
검색이 완료되고 동영상을 재생할 준비가 된 후에도 동일한 현상이 발생합니다.
이 방법으로 일부 구성 요소가 로드될 때 UI에 재생/일시 중지 버튼 대신 로딩 표시기를 표시할 수 있습니다.
CSS, GSAP 및 GSAP의 TextSplitter를 사용하면 텍스트 요소로 정말 놀라운 작업을 수행할 수 있습니다.
기본 캔버스 텍스트 요소는 제한되어 있으며, 우리 앱의 기본 사용 사례가 소셜 미디어용 짧은 동영상을 만드는 것이기 때문에 적합하지 않습니다.
다행히도 거의 모든 HTML 텍스트를 캔버스로 렌더링하는 방법을 찾았는데, 이는 비디오 출력을 렌더링하는 데 매우 중요합니다.
픽시 HTML텍스트
이것이 가장 간단한 해결책이었을 것입니다. 안타깝게도 저에게는 효과가 없었습니다.
GSAP로 HTML 텍스트에 애니메이션을 적용할 때 속도가 상당히 느려졌고, 제가 사용해 본 Google 글꼴도 많이 지원하지 않았습니다.
사토리
Satori는 놀랍습니다. 좀 더 간단한 사용 사례에 사용되는 모습을 상상할 수 있습니다. 안타깝게도 일부 GSAP 애니메이션은 Satori와 호환되지 않는 스타일을 변경하여 오류가 발생합니다.
이물질이 포함된 SVG
드디어 이를 해결하기 위한 맞춤형 솔루션을 만들었습니다.
어려운 부분은 이모티콘과 맞춤 글꼴을 지원하는 것이었지만 해결했습니다.
다음과 같은 SVG를 생성하는 generateSVG 메소드가 있는 SVGGenerator 클래스를 만들었습니다.
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" version="1.1">${styleTag}<foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="transform-origin: 0 0;">${html}</div></foreignObject></svg>
styleTag는 다음과 같습니다.
<style>@font-face { font-family: ${fontFamilyName}; src: url('${fontData}') }</style>
이 작업을 수행하려면 전달하는 HTML의 인라인 스타일 내에 올바른 글꼴 모음이 설정되어 있어야 합니다. 글꼴 데이터는 data:font/ttf;base64,longboringstring
과 같은 base64로 인코딩된 데이터 문자열이어야 합니다.상속보다 구성이라는 말이 있죠.
손을 더럽히기 위한 연습으로 상속 기반 접근 방식에서 후크 기반 시스템으로 리팩토링했습니다.
제 영상 편집기에서는 VIDEO, AUDIO, TEXT, SUBTITLES, IMAGE, SHAPE 등과 같은 요소를 구성요소라고 부릅니다.
이 글을 다시 작성하기 전에는 추상 클래스인 BaseComponent가 있었고 각 구성 요소 클래스가 이를 확장했기 때문에 VideoComponent에는 동영상 등에 대한 로직이 있었습니다.
문제는 너무 빨리 엉망이 되어버렸다는 것입니다.
구성 요소는 렌더링 방식, Pixi 텍스처 관리 방식, 애니메이션 방식 등을 담당했습니다.
이제 컴포넌트 클래스가 하나만 있어서 매우 간단합니다.
이제 4개의 수명 주기 이벤트가 있습니다.
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" version="1.1">${styleTag}<foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="transform-origin: 0 0;">${html}</div></foreignObject></svg>
이 구성 요소 클래스에는 동작을 변경하는 addHook라는 메서드가 있습니다.
후크는 구성 요소 수명 주기 이벤트에 연결되어 작업을 수행할 수 있습니다.
예를 들어 제가 비디오 및 오디오 구성 요소에 사용하는 MediaHook이 있습니다.
MediaHook은 기본 오디오 또는 비디오 요소를 생성하고 자동으로 기본 타임라인과 동기화를 유지합니다.
빌딩 컴포넌트는 디렉터 패턴과 함께 빌더 패턴을 사용했습니다(참고자료 참조).
이런 식으로 오디오 구성 요소를 만들 때 여기에 MediaHook을 추가하고 비디오 구성 요소에도 추가합니다. 그러나 다음과 같은 경우에도 동영상에 추가 후크가 필요합니다.
이 접근 방식을 사용하면 렌더링 논리나 구성 요소가 장면에서 작동하는 방식을 매우 쉽게 변경, 확장 또는 수정할 수 있습니다.
가장 빠르고 비용 효율적인 방법으로 동영상을 렌더링하는 방법에 대해 여러 가지 접근 방식을 시도했습니다.
2020년에는 프레임을 하나씩 렌더링하는 가장 간단한 접근 방식으로 시작했는데, 이는 많은 도구에서 수행하는 작업입니다.
몇 번의 시행착오 끝에 렌더링 레이어 접근 방식으로 전환했습니다.
즉, SceneData 문서에는 구성요소를 포함하는 레이어가 포함되어 있습니다.
이러한 각 레이어는 별도로 렌더링된 후 ffmpeg와 결합되어 최종 출력을 생성합니다.
레이어에는 동일한 유형의 구성요소만 포함될 수 있다는 제한이 있었습니다.
예를 들어 비디오가 포함된 레이어에는 텍스트 요소가 포함될 수 없습니다. 다른 동영상만 포함할 수 있습니다.
이에는 분명히 장단점이 있습니다.
Lambda에서 애니메이션이 포함된 HTML 텍스트를 독립적으로 렌더링하고 투명한 비디오로 변환한 다음 최종 출력을 위해 다른 청크와 결합하는 것은 매우 간단했습니다.
반면, 비디오 구성 요소가 포함된 레이어는 ffmpeg로 간단하게 처리되었습니다.
그러나 이 접근 방식에는 큰 단점이 있었습니다.
비디오 크기 조절, 페이드 또는 회전을 위한 키프레임 시스템을 구현하려면 fluent-ffmpeg에서 이러한 기능을 포트해야 합니다.
그것은 확실히 가능합니다. 하지만 제가 맡은 다른 모든 책임 때문에 저는 그것을 하지 못했습니다.
그래서 저는 첫 번째 접근 방식, 즉 한 프레임씩 렌더링하는 방식으로 돌아가기로 결정했습니다.
렌더링 요청은 Express를 통해 백엔드 서버로 전송됩니다.
이 경로는 동영상이 아직 렌더링되지 않았는지 확인하고, 렌더링되지 않은 경우 BullMQ 대기열에 추가됩니다.
큐가 렌더링 처리를 시작한 후 헤드리스 Chrome의 여러 인스턴스가 생성됩니다.
참고: 이 처리는 AMD EPYC 7502P 32코어 프로세서 및 128GB RAM을 갖춘 전용 Hetzner 서버에서 발생하므로 성능이 매우 뛰어난 시스템입니다.
Chromium에는 코덱이 없으므로 Playwright를 사용하므로 Chrome 설치가 간편합니다.
그런데 왜인지 영상 프레임이 검게 나오더군요.
제가 뭔가를 놓친 게 분명합니다. 하지만 비디오를 사용하는 대신 비디오 구성 요소를 개별 이미지 프레임으로 분할하고 이를 서버리스 브라우저에서 사용하기로 결정했습니다.
그래도 가장 중요한 점은 스크린샷 방식을 사용하지 않는 것이었습니다.
하나의 캔버스에 모든 것이 있으므로 캔버스에서 .getDataURL()을 사용하여 이미지로 가져올 수 있으며 이는 훨씬 빠릅니다.
이 작업을 더 간단하게 하기 위해 동영상 편집기를 번들로 묶고 창에 몇 가지 기능을 추가하는 정적 페이지를 만들었습니다.
이것은 Playwright/Puppeteer로 로드되고 각 프레임에서 간단히 다음을 호출합니다.
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" version="1.1">${styleTag}<foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="transform-origin: 0 0;">${html}</div></foreignObject></svg>
이미지로 저장하거나 버퍼에 추가하여 비디오 청크를 렌더링할 수 있는 프레임 데이터를 제공합니다.
이 전체 프로세스는 비디오 길이에 따라 5~10명의 작업자로 분할되어 최종 출력에 병합됩니다.
대신 Lambda 같은 것으로 오프로드할 수도 있지만 저는 RunPod를 사용하는 쪽으로 기울고 있습니다. 서버리스 아키텍처의 유일한 단점은 Python을 사용한다는 점인데, 저는 그다지 익숙하지 않습니다.
이렇게 하면 렌더링이 클라우드에서 처리되는 여러 청크로 분할될 수 있으며, 60분짜리 비디오의 렌더링도 1~2분 안에 완료될 수 있습니다. 있으면 좋지만 이는 우리의 주요 목표나 사용 사례가 아닙니다.
Pixi 8에서 Pixi 7로 다운그레이드한 이유는 Pixi 7에도 2D 캔버스를 지원하는 '레거시' 버전이 있기 때문입니다. 렌더링 속도가 훨씬 빠릅니다. 60초짜리 동영상을 서버에서 렌더링하는 데 약 80초가 걸리지만, 캔버스에 WebGL이나 WebGPU 컨텍스트가 있는 경우 초당 1~2프레임만 렌더링할 수 있었습니다.
흥미롭게도 서버리스 Chrome은 WebGL 캔버스를 렌더링할 때 헤드풀 Firefox에 비해 훨씬 느렸습니다.
전용 GPU를 사용해도 렌더링 속도를 크게 높이는 데 도움이 되지 않았습니다. 제가 뭔가 잘못했거나 단순히 헤드리스 Chrome이 WebGL에서 그다지 성능이 좋지 않은 것 같습니다.
우리 사용 사례의 WebGL은 일반적으로 매우 짧은 전환에 적합합니다.
이와 관련해 테스트할 계획 중 하나는 WebGL 청크와 WebGL이 아닌 청크를 별도로 렌더링하는 것입니다.
프로젝트에는 여러 부분이 참여합니다.
문서의 구조가 스키마 없는 데이터베이스에 저장되는 것이 가장 적합하기 때문에 장면 데이터는 MongoDB에 저장됩니다.
SvelteKit으로 작성된 프런트엔드는 urql을 GraphQL 클라이언트로 사용합니다.
GraphQL 서버는 MongoDB 및 놀라운 Lighthouse GraphQL과 함께 PHP Laravel을 사용합니다.
하지만 이건 아마도 다음번 주제일지도 모르겠습니다.
그럼 지금은 끝이에요! 이것을 제작에 투입하고 현재의 영상 편집기를 교체하기 전에 해야 할 일이 많이 있는데, 꽤 버그가 많고 약간 프랑켄슈타인이 생각납니다.
당신의 생각을 알려주시고 계속해서 즐겨주세요!
위 내용은 개인 개발자로서 TypeScript 비디오 편집기 구축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!