이제 크리에이터는 강력한 API (IndexedDB 및 WebCodecs 등)와 성능 도구를 갖춘 Kapwing을 사용하여 웹에서 고품질 동영상 콘텐츠를 편집할 수 있습니다.
팬데믹이 시작된 이후 온라인 동영상 소비가 급격히 증가했습니다. 사람들은 더 많은 시간을 TV에서 무한한 고화질 동영상을 TikTok, Instagram, YouTube 등의 플랫폼을 예로 들 수 있습니다 광고 소재 및 소규모 비즈니스 전 세계 동영상 소유자는 쉽고 빠르게 사용할 수 있는 있습니다.
Kapwing과 같은 회사는 모든 동영상 콘텐츠를 제대로 제작할 수 있도록 지원합니다. 최신 강력한 API와 성능 도구를 사용하여 웹에서 할 수 있습니다.
Kapwing 정보
Kapwing은 주로 캐주얼한 동영상을 위해 설계된 웹 기반 공동작업 동영상 편집기입니다. 게임 스트림 게시자, 뮤지션, YouTube 크리에이터, 밈 동영상 등의 광고 소재를 사용합니다 그것은 또한 비즈니스 소유자가 유용한 정보를 얻을 수 있는 자신의 소셜 콘텐츠(Facebook 및 Instagram 광고 등)
사람들은 특정 작업을 검색하여 Kapwing을 발견합니다. 예를 들어 동영상 자르기'와 같은 "동영상에 음악 추가해 줘" 또는 '동영상 크기 조정'을 참조하세요. 그들은 무엇을 할 수 있는지 광고를 클릭 한 번으로 검색할 수 있었습니다. 앱 스토어로 이동하여 앱을 다운로드하는 사용자 웹을 사용하면 사람들이 도움이 필요한 작업을 정확히 검색하여 그 작업을 수행할 수 있습니다.
첫 번째 클릭 이후 Kapwing 사용자는 훨씬 더 많은 것을 할 수 있습니다. 사용자는 할 수 있습니다. 무료 템플릿 탐색, 무료 스톡 동영상의 새로운 레이어 추가, 삽입 동영상 자막, 동영상 스크립트 작성, 배경 음악 업로드 등이 포함됩니다.
Kapwing이 웹에 실시간 수정 및 공동작업을 제공하는 방법
웹은 고유한 장점이 있지만 해결할 수 있습니다 Kapwing은 복잡한 오디오, 동영상, 오디오북을 다중 계층 프로젝트를 수행하는 데 중점을 두고 있습니다. 이를 위해 다양한 웹 API를 사용하여 성능과 특성 목표를 달성할 수 있습니다
IndexedDB
고성능 편집을 사용하려면 모든 사용자의 콘텐츠가 클라이언트에 있어야 하며 가능하면 네트워크를 사용하지 않아야 합니다. 스트리밍 서비스와 달리 일반적으로 사용자는 한 콘텐츠에 한 번 액세스하는 경우 업로드 후 며칠, 심지어 몇 개월이 지난 후에도
IndexedDB를 사용하면 영구 파일을 제공할 수 있습니다. 사용자에게 제공할 수 있습니다 그 결과, 미디어의 90% 이상이 앱의 요청은 로컬에서 처리됩니다 IndexedDB를 시스템에 통합하는 것은 매우 간단했습니다.
다음은 앱 로드 시 실행되는 상용구 초기화 코드입니다.
import {DBSchema, openDB, deleteDB, IDBPDatabase} from 'idb';
let openIdb: Promise <IDBPDatabase<Schema>>;
const db =
(await openDB) <
Schema >
(
'kapwing',
version, {
upgrade(db, oldVersion) {
if (oldVersion >= 1) {
// assets store schema changed, need to recreate
db.deleteObjectStore('assets');
}
db.createObjectStore('assets', {
keyPath: 'mediaLibraryID'
});
},
async blocked() {
await deleteDB('kapwing');
},
async blocking() {
await deleteDB('kapwing');
},
}
);
버전을 전달하고 upgrade
함수를 정의합니다. 다음에 사용됩니다.
필요에 따라 스키마를 업데이트할 수 있습니다 오류 처리를 전달합니다.
blocked
, blocking
등의 콜백 메서드를
불안정한 시스템을 사용하는 사용자의 문제를 방지합니다.
마지막으로 기본 키 keyPath
의 정의를 확인하세요. 여기서는 mediaLibraryID
라고 하는 고유 ID입니다. 사용자가 업로더를 통해서든 타사 확장 프로그램을 통해서든 사용자가 시스템에 미디어를 추가하면 YouTube는 해당 미디어를 추가합니다.
미디어 라이브러리에 추가합니다.
export async function addAsset(mediaLibraryID: string, file: File) {
return runWithAssetMutex(mediaLibraryID, async () => {
const assetAlreadyInStore = await (await openIdb).get(
'assets',
mediaLibraryID
);
if (assetAlreadyInStore) return;
const idbVideo: IdbVideo = {
file,
mediaLibraryID,
};
await (await openIdb).add('assets', idbVideo);
});
}
runWithAssetMutex
는 내부적으로 정의된 함수로
IndexedDB 액세스 IndexedDB API는 비동기식이므로 읽기-수정-쓰기 유형 작업에 필요합니다.
이제 파일에 액세스하는 방법을 살펴보겠습니다. 다음은 getAsset
함수입니다.
export async function getAsset(
mediaLibraryID: string,
source: LayerSource | null | undefined,
location: string
): Promise<IdbAsset | undefined> {
let asset: IdbAsset | undefined;
const { idbCache } = window;
const assetInCache = idbCache[mediaLibraryID];
if (assetInCache && assetInCache.status === 'complete') {
asset = assetInCache.asset;
} else if (assetInCache && assetInCache.status === 'pending') {
asset = await new Promise((res) => {
assetInCache.subscribers.push(res);
});
} else {
idbCache[mediaLibraryID] = { subscribers: [], status: 'pending' };
asset = (await openIdb).get('assets', mediaLibraryID);
idbCache[mediaLibraryID].asset = asset;
idbCache[mediaLibraryID].subscribers.forEach((res: any) => {
res(asset);
});
delete (idbCache[mediaLibraryID] as any).subscribers;
if (asset) {
idbCache[mediaLibraryID].status = 'complete';
} else {
idbCache[mediaLibraryID].status = 'failed';
}
}
return asset;
}
IndexedDB를 최소화하는 데 사용되는 자체 데이터 구조인 idbCache
가 있습니다.
액세스할 수 있습니다 IndexedDB는 빠른 반면 로컬 메모리 액세스는 더 빠릅니다.
캐시 크기를 관리하는 한 이 방법을 사용하는 것이 좋습니다.
subscribers
배열은 다음 항목에 대한 동시 액세스를 방지하는 데 사용됩니다.
IndexedDB). 그렇지 않으면 로드 시 일반적입니다.
웹 오디오 API
오디오 시각화는 동영상 편집에 매우 중요합니다. 이해하다 편집기에서 스크린샷을 살펴보세요.
이 동영상은 YouTube 앱에서 흔히 사용되는 YouTube 스타일 동영상입니다. 사용자가 움직임이 너무 많아 타임라인의 시각적인 썸네일이 섹션 간 탐색에 유용합니다. 반면에 오디오는 파형은 피크와 밸리를 나타내며 골짜기가 보통 기록의 데드 타임에 해당합니다 타임라인을 확대하면 끊김 및 일시중지에 해당하는 골짜기가 있는 더 세분화된 오디오 정보가 표시됩니다.
YouTube의 사용자 연구에 따르면 많은 크리에이터가 이러한 파형의 영향을 받는다고 합니다. 그들은 콘텐츠를 합칩니다. 웹 오디오 API를 사용하면 이미지를 확대/축소하거나 화면 이동 시 빠르게 업데이트할 수 있습니다. 확인할 수 있습니다
아래 스니펫은 이 작업을 수행하는 방법을 보여줍니다.
const getDownsampledBuffer = (idbAsset: IdbAsset) =>
decodeMutex.runExclusive(
async (): Promise<Float32Array> => {
const arrayBuffer = await idbAsset.file.arrayBuffer();
const audioContext = new AudioContext();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
const offline = new OfflineAudioContext(
audioBuffer.numberOfChannels,
audioBuffer.duration * MIN_BROWSER_SUPPORTED_SAMPLE_RATE,
MIN_BROWSER_SUPPORTED_SAMPLE_RATE
);
const downsampleSource = offline.createBufferSource();
downsampleSource.buffer = audioBuffer;
downsampleSource.start(0);
downsampleSource.connect(offline.destination);
const downsampledBuffer22K = await offline.startRendering();
const downsampledBuffer22KData = downsampledBuffer22K.getChannelData(0);
const downsampledBuffer = new Float32Array(
Math.floor(
downsampledBuffer22KData.length / POST_BROWSER_SAMPLE_INTERVAL
)
);
for (
let i = 0, j = 0;
i < downsampledBuffer22KData.length;
i += POST_BROWSER_SAMPLE_INTERVAL, j += 1
) {
let sum = 0;
for (let k = 0; k < POST_BROWSER_SAMPLE_INTERVAL; k += 1) {
sum += Math.abs(downsampledBuffer22KData[i + k]);
}
const avg = sum / POST_BROWSER_SAMPLE_INTERVAL;
downsampledBuffer[j] = avg;
}
return downsampledBuffer;
}
);
IndexedDB에 저장된 애셋에 이 도우미를 전달합니다. 완료되면 IndexedDB의 자산과 자체 캐시를 업데이트합니다.
AudioContext
생성자를 사용하여 audioBuffer
에 관한 데이터를 수집합니다.
기기 하드웨어에 렌더링하지 않기 때문에
OfflineAudioContext
를 사용하여 ArrayBuffer
에 렌더링하고 여기에 저장합니다.
진폭 데이터입니다.
API 자체는 요청 시 필요한 것보다 훨씬 높은 샘플링 레이트로 효과적으로 시각화할 수 있습니다. 그렇기 때문에 수동으로 200Hz로 다운샘플링했습니다. 유용하고 시각적으로 매력적인 파형에 충분한 것으로 나타났습니다.
WebCodecs
특정 동영상의 경우 트랙 썸네일이 타임라인에 더 유용함 탐색이 더 효율적입니다. 하지만 썸네일을 생성하는 것이 훨씬 집약적입니다.
로드 시 모든 잠재적 썸네일을 캐시할 수 없으므로 타임라인에 빠르게 디코딩할 수 없습니다. 화면 이동/확대/축소는 성능이 우수하고 반응하는 애플리케이션에 매우 중요합니다. 이 원활한 프레임 그리기를 달성하는 데 병목 현상이 발생하는 이유는 프레임을 디코딩하는 것입니다. 최근에는 HTML5 동영상 플레이어를 사용했습니다. 이 접근 방식의 성능은 안정적이지 않았으며 프레임 렌더링 중에 앱 응답성이 저하되는 경우가 많았습니다.
최근에WebCodecs 있습니다. 이렇게 하면 대형 썸네일을 더 잘 그릴 수 있습니다. 레이어의 양을 처리하는 데 사용할 수 있습니다. 웹에서는 작업자 구현이 아직 진행 중이므로 기존 기본 스레드를 구현합니다.
동영상 파일에는 동영상, 오디오, 자막 등 여러 스트림이 포함되어 있습니다. '복합'되어 있음 있습니다. WebCodecs를 사용하려면 먼저 역다중화된 동영상이 있어야 합니다. 있습니다. 다음과 같이 mp4box 라이브러리를 사용하여 mp4를 역다중화합니다.
async function create(demuxer: any) {
demuxer.file = (await MP4Box).createFile();
demuxer.file.onReady = (info: any) => {
demuxer.info = info;
demuxer._info_resolver(info);
};
demuxer.loadMetadata();
}
const loadMetadata = async () => {
let offset = 0;
const asset = await getAsset(this.mediaLibraryId, null, this.url);
const maxFetchOffset = asset?.file.size || 0;
const end = offset + FETCH_SIZE;
const response = await fetch(this.url, {
headers: { range: `bytes=${offset}-${end}` },
});
const reader = response.body.getReader();
let done, value;
while (!done) {
({ done, value } = await reader.read());
if (done) {
this.file.flush();
break;
}
const buf: ArrayBufferLike & { fileStart?: number } = value.buffer;
buf.fileStart = offset;
offset = this.file.appendBuffer(buf);
}
};
이 스니펫은 다음을 캡슐화하는 데 사용하는 demuxer
클래스를 참조합니다.
인터페이스를 MP4Box
로 전달합니다. 다시 IndexedDB에서 애셋에 액세스합니다. 이러한
세그먼트는 반드시 바이트 순서로 저장되지는 않으며 appendBuffer
는
메서드가 다음 청크의 오프셋을 반환합니다.
다음은 동영상 프레임을 디코딩하는 방법입니다.
const getFrameFromVideoDecoder = async (demuxer: any): Promise<any> => {
let desiredSampleIndex = demuxer.getFrameIndexForTimestamp(this.frameTime);
let timestampToMatch: number;
let decodedSample: VideoFrame | null = null;
const outputCallback = (frame: VideoFrame) => {
if (frame.timestamp === timestampToMatch) decodedSample = frame;
else frame.close();
};
const decoder = new VideoDecoder({
output: outputCallback,
});
const {
codec,
codecWidth,
codecHeight,
description,
} = demuxer.getDecoderConfigurationInfo();
decoder.configure({ codec, codecWidth, codecHeight, description });
/* begin demuxer interface */
const preceedingKeyFrameIndex = demuxer.getPreceedingKeyFrameIndex(
desiredSampleIndex
);
const trak_id = demuxer.trak_id
const trak = demuxer.moov.traks.find((trak: any) => trak.tkhd.track_id === trak_id);
const data = await demuxer.getFrameDataRange(
preceedingKeyFrameIndex,
desiredSampleIndex
);
/* end demuxer interface */
for (let i = preceedingKeyFrameIndex; i <= desiredSampleIndex; i += 1) {
const sample = trak.samples[i];
const sampleData = data.readNBytes(
sample.offset,
sample.size
);
const sampleType = sample.is_sync ? 'key' : 'delta';
const encodedFrame = new EncodedVideoChunk({
sampleType,
timestamp: sample.cts,
duration: sample.duration,
samapleData,
});
if (i === desiredSampleIndex)
timestampToMatch = encodedFrame.timestamp;
decoder.decodeEncodedFrame(encodedFrame, i);
}
await decoder.flush();
return { type: 'value', value: decodedSample };
};
디뮤서의 구조는 상당히 복잡하며 이 모듈의 범위를 벗어납니다.
도움말을 참조하세요. 각 프레임을 samples
라는 배열에 저장합니다. 디멀티플렉서(demuxer)를
원하는 타임스탬프에 가장 가까운 선행 키프레임을 찾습니다.
여기서 동영상 디코딩을 시작해야 합니다.
동영상은 키 프레임 또는 i-frame으로 알려진 풀 프레임과 작은 델타 프레임으로, 보통 p- 또는 b-프레임이라고도 합니다. 디코딩은 항상 확인할 수 있습니다
애플리케이션은 다음을 기준으로 프레임을 디코딩합니다.
- 프레임 출력 콜백을 사용하여 디코더를 인스턴스화합니다.
- 특정 코덱 및 입력 해상도에 맞게 디코더 구성
- 디뮤셔의 데이터를 사용하여
encodedVideoChunk
를 만듭니다. decodeEncodedFrame
메서드 호출
원하는 타임스탬프가 있는 프레임에 도달할 때까지 이 작업을 수행합니다.
다음 단계
프런트엔드에서 확장이란 프로젝트가 더 크고 복잡해질 때 정확하고 성능이 우수한 재생을 유지하는 기능을 말합니다. 확장의 한 가지 방법 한 번에 가능한 한 적은 동영상을 마운트하는 것이 좋지만 이로 인해 전환이 느리고 끊김이 발생할 위험이 있습니다. 재사용을 위해 동영상 구성요소를 캐시하는 내부 시스템을 개발했지만 HTML5 동영상 태그가 제공할 수 있는 제어 수준에는 제한이 있습니다.
향후 WebCodecs를 사용하여 모든 미디어 재생을 시도할 수 있습니다. 이를 통해 버퍼링할 데이터를 매우 정확하게 지정할 수 있으므로 성능을 확장하는 데 도움이 됩니다.
또한 대규모 트랙패드 계산을 오프로드하여 웹 작업자를 아우르며, 더욱 스마트하게 프리패치를 사용할 수 있게 되었습니다. 사전 생성된 프레임 등이 포함됩니다. 전반적인 애플리케이션 성능을 최적화하고 WebGL과 같은 도구로 기능을 확장할 수 있는 큰 기회가 있습니다.
현재 스마트 배경 삭제에 사용 중인 TensorFlow.js에 대한 투자를 계속할 계획입니다. TensorFlow.js를 다른 솔루션에도 객체 인식, 특성 추출, 스타일 전송 등과 같은 정교한 작업을 수행할 수 있습니다.
궁극적으로 네이티브 스타일로 제품을 계속 빌드할 수 있게 되어 성능 및 기능성에 대해 알아봅니다.