Kapwing: 웹을 위한 강력한 동영상 편집

이제 크리에이터는 강력한 API (IndexedDB 및 WebCodecs 등)와 성능 도구를 갖춘 Kapwing을 사용하여 웹에서 고품질 동영상 콘텐츠를 편집할 수 있습니다.

Joshua Grossberg
Joshua Grossberg

팬데믹이 시작된 이후 온라인 동영상 소비가 급격히 증가했습니다. 사람들은 더 많은 시간을 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

오디오 시각화는 동영상 편집에 매우 중요합니다. 이해하다 편집기에서 스크린샷을 살펴보세요.

Kapwing의 편집자에는 여러 템플릿과 맞춤 요소가 포함된 미디어 메뉴가 있습니다. LinkedIn과 같은 특정 플랫폼에만 해당하는 일부 템플릿이 포함되어 있습니다. 동영상, 오디오, 애니메이션을 구분하는 타임라인 내보내기 품질 옵션이 있는 캔버스 편집기 동영상 미리보기 더 많은 기능을 제공합니다

이 동영상은 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-프레임이라고도 합니다. 디코딩은 항상 확인할 수 있습니다

애플리케이션은 다음을 기준으로 프레임을 디코딩합니다.

  1. 프레임 출력 콜백을 사용하여 디코더를 인스턴스화합니다.
  2. 특정 코덱 및 입력 해상도에 맞게 디코더 구성
  3. 디뮤셔의 데이터를 사용하여 encodedVideoChunk를 만듭니다.
  4. decodeEncodedFrame 메서드 호출

원하는 타임스탬프가 있는 프레임에 도달할 때까지 이 작업을 수행합니다.

다음 단계

프런트엔드에서 확장이란 프로젝트가 더 크고 복잡해질 때 정확하고 성능이 우수한 재생을 유지하는 기능을 말합니다. 확장의 한 가지 방법 한 번에 가능한 한 적은 동영상을 마운트하는 것이 좋지만 이로 인해 전환이 느리고 끊김이 발생할 위험이 있습니다. 재사용을 위해 동영상 구성요소를 캐시하는 내부 시스템을 개발했지만 HTML5 동영상 태그가 제공할 수 있는 제어 수준에는 제한이 있습니다.

향후 WebCodecs를 사용하여 모든 미디어 재생을 시도할 수 있습니다. 이를 통해 버퍼링할 데이터를 매우 정확하게 지정할 수 있으므로 성능을 확장하는 데 도움이 됩니다.

또한 대규모 트랙패드 계산을 오프로드하여 웹 작업자를 아우르며, 더욱 스마트하게 프리패치를 사용할 수 있게 되었습니다. 사전 생성된 프레임 등이 포함됩니다. 전반적인 애플리케이션 성능을 최적화하고 WebGL과 같은 도구로 기능을 확장할 수 있는 큰 기회가 있습니다.

현재 스마트 배경 삭제에 사용 중인 TensorFlow.js에 대한 투자를 계속할 계획입니다. TensorFlow.js를 다른 솔루션에도 객체 인식, 특성 추출, 스타일 전송 등과 같은 정교한 작업을 수행할 수 있습니다.

궁극적으로 네이티브 스타일로 제품을 계속 빌드할 수 있게 되어 성능 및 기능성에 대해 알아봅니다.