Kapwing: تعديل الفيديوهات بفعالية على الويب

أصبح بإمكان صنّاع المحتوى الآن تعديل محتوى الفيديو العالي الجودة على الويب باستخدام Kapwing، وذلك بفضل واجهات برمجة التطبيقات الفعّالة (مثل IndexedDB وWebCodecs) وأدوات الأداء.

Joshua Grossberg
Joshua Grossberg

ازدادت مشاهدة الفيديوهات على الإنترنت بشكل سريع منذ بداية الجائحة. يقضي المستخدمون وقتًا أطول في مشاهدة فيديوهات عالية الجودة على منصات مثل TikTok وInstagram وYouTube. تصميمات الإعلانات والأنشطة التجارية الصغيرة يحتاج مالكو المواقع حول العالم إلى أدوات سريعة وسهلة الاستخدام لإنشاء فيديوهات المحتوى.

تتيح شركات مثل Kapwing لصنّاع المحتوى إنشاء كل هذا المحتوى بشكل صحيح. على الويب، باستخدام أحدث الميزات الفعّالة لواجهات برمجة التطبيقات وأدوات الأداء.

لمحة عن Kapwing

Kapwing هو محرر فيديو تعاوني مستنِد إلى الويب تم تصميمه بشكل أساسي للمهام الإبداعيين مثل منشئي الألعاب والموسيقيين وصنّاع المحتوى على YouTube والمشاركات المضحكة من المهم أيضًا مصدرًا موثوقًا لأصحاب الأنشطة التجارية الذين يحتاجون إلى طريقة سهلة لإنتاج المحتوى الاجتماعي الخاص بها، مثل إعلانات Facebook وInstagram.

يكتشف الأشخاص Kapwing من خلال البحث عن مهمة محددة، على سبيل المثال "كيفية قطع فيديو" و"إضافة موسيقى إلى الفيديو الخاص بي" أو "تغيير حجم الفيديو". يمكنه فعل ما فقد بحثوا عنه بنقرة واحدة فقط - دون أي معوقات الانتقال إلى متجر التطبيقات وتنزيل تطبيق. يسهل الويب على من الأشخاص البحث عن المهمة التي يحتاجون إلى المساعدة بشأنها بالضبط، ثم القيام بذلك.

وبعد النقرة الأولى، يمكن لمستخدمي Kapwing إجراء المزيد من المهام. يمكنه استكشاف نماذج مجانية، إضافة طبقات جديدة من الفيديوهات الجاهزة المجانية، إدراج الترجمة وتحويل الصوت إلى نص في الفيديوهات وتحميل موسيقى الخلفية

قصة نجاح Kapwing في توفير إمكانية التعديل والتعاون في الوقت الفعلي على الويب

على الرغم من أنّ الويب يوفّر مزايا فريدة، إلا أنّه يطرح أيضًا تحديات مختلفة. تحتاج Kapwing إلى تقديم تشغيل سلس ودقيق للإعلانات المعقدة متعددة الطبقات على مستوى مجموعة كبيرة من الأجهزة وحالات الشبكة. لتحقيق ذلك، نستخدم مجموعة متنوعة من واجهات برمجة التطبيقات للويب من أجل تحقيق أهداف الأداء والميزات.

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. عندما يضيف مستخدم مادة من الوسائط إلى نظامنا، سواء من خلال برنامج التحميل أو إضافة تابعة لجهة خارجية، نضيف الوسائط إلى مكتبة الوسائط باستخدام الرمز التالي:

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 غير متزامنة.

الآن دعونا نلقي نظرة على كيفية الوصول إلى الملفات. في ما يلي دالة 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;
}

ولدينا بنية بيانات خاصة بنا، idbCache، تُستخدَم لتقليل قاعدة البيانات المفهرسة. عمليات الدخول. على الرغم من أنّ أداة IndexedDB سريعة، يكون الوصول إلى الذاكرة المحلية أسرع. ننصح بهذا النهج ما دام بإمكانك إدارة حجم ذاكرة التخزين المؤقت.

المصفوفة subscribers، وهي تُستخدم لمنع الوصول المتزامن إلى قاعدة البيانات المفهرسة ستكون شائعة عند التحميل.

واجهة برمجة تطبيقات Web Audio

إنّ المرئيات الصوتية مهمة للغاية لتعديل الفيديوهات. لفهم ألقِ نظرة على لقطة شاشة من المحرر:

يحتوي محرر Kapwing على قائمة للوسائط، بما في ذلك العديد من القوالب والعناصر المخصصة، بما في ذلك بعض القوالب الخاصة بمنصات معينة مثل LinkedIn؛ جدول زمني يفصل بين الفيديو والصوت والرسوم المتحركة؛ محرر لوحة الرسم مع خيارات جودة التصدير معاينة للفيديو؛ والمزيد من الإمكانات.

هذا الفيديو من نمط YouTube، وهو أمر شائع في تطبيقنا. لا يعرف المستخدم خلال المقطع، وبالتالي لا تظهر الصور المصغّرة المرئية للجداول الزمنية مفيدة للتنقل بين الأقسام. من ناحية أخرى، يتوافق الصوت يعرض الشكل الموجي الارتفاعات والوديان مع الوديان التي تتجاوب عادةً مع الوقت المنقضي في التسجيل. إذا كنت يمكنك تكبير المخطط الزمني، وسترى المزيد من المعلومات الصوتية الأكثر دقة باستخدام والوديان المقابلة للتعثر والتوقفات.

تشير أبحاث المستخدمين إلى أنّ هذه الأشكال الموجية تستند عادةً إلى أعمال صنّاع المحتوى يقومون بوصل المحتوى الخاص بهم. وتتيح لنا واجهة برمجة التطبيقات Web audio 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 وكذلك ذاكرة التخزين المؤقت الخاصة بنا.

نجمع بيانات عن audioBuffer باستخدام الدالة الإنشائية AudioContext، ولكن نظرًا لأننا لا نعرض الجهاز على الأجهزة، فإننا نستخدم OfflineAudioContext لعرضه على ArrayBuffer حيث سنخزنه بيانات السعة.

تقوم واجهة برمجة التطبيقات نفسها بعرض البيانات بمعدل عينة أعلى بكثير من المطلوب التصور الفعال. ولهذا السبب نقوم يدويًا بتخفيض العيّنات إلى 200 هرتز، كافية لإنشاء أشكال موجية مفيدة وجذابة بصريًا

WebCodecs

تكون الصور المصغّرة لبعض الفيديوهات مفيدة أكثر بالنسبة إلى المخطط الزمني. التنقل من الأشكال الموجية. ومع ذلك، يشكّل إنشاء صور مصغّرة مصدرًا أكثر مقارنةً بتوليد الأشكال الموجية

لا يمكننا تخزين كل صورة مصغّرة محتملة مؤقتًا أثناء التحميل، لذا يمكنك فك ترميز المخطط الزمني بسرعة العرض الشامل/التكبير/التصغير أمر بالغ الأهمية لتطبيق ذي أداء سريع الاستجابة. تشير رسالة الأشكال البيانية والذي يعرقل تحقيق رسم سلس للإطارات هو فك ترميز الإطارات، والذي استخدمنا مؤخرًا مشغل فيديو HTML5. وأداء هذا النهج لم يكُن الجهاز موثوقًا به، ولاحظنا في كثير من الأحيان انخفاضًا في استجابة التطبيق أثناء عرض اللقطة. العرض.

انتقلنا مؤخرًا إلى WebCodecs الذي يمكن استخدامه في العاملين على الويب. يعزز ذلك قدرتنا على رسم صور مصغرة لأحجام مقدار الطبقات بدون التأثير في أداء سلسلة التعليمات الرئيسية. في حين أنّ عملية تنفيذ web worker لا تزال جارية، نقدّم أدناه مخطّطًا تفصيليًا لتنفيذ السلسلة الرئيسية الحالية.

يتضمّن ملف الفيديو مجموعات بث متعددة: فيديو والصوت وترجمات وما إلى ذلك. "متخللة" لاستخدام WebCodecs، يجب أولاً أن يكون لدينا بث فيديو مفكَّك الترميز. نزيل ترميز ملفات mp4 باستخدام مكتبة mp4box، كما هو موضّح هنا:

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. نستخدم أداة إزالة التشويش للعثور على أقرب إطار رئيسي سابق للطابع الزمني المطلوب، وهو حيث يجب أن نبدأ فك ترميز الفيديو.

تتكوّن الفيديوهات من إطارات كاملة، تُعرف باسم الإطارات الرئيسية أو الإطارات i، بالإضافة إلى إطارات دلتا أصغر حجمًا، والتي يُشار إليها غالبًا باسم الإطارات p أو b. يجب دائمًا فك الترميز تبدأ في إطار رئيسي.

يفكّ التطبيق ترميز اللقطات من خلال:

  1. إنشاء مثيل لبرنامج فك الترميز باستخدام استدعاء إخراج الإطار
  2. تهيئة برنامج فك الترميز لبرنامج الترميز المحدد ودرجة دقة الإدخال.
  3. إنشاء encodedVideoChunk باستخدام بيانات من أداة فك الترميز
  4. جارٍ استدعاء الطريقة decodeEncodedFrame.

نواصل ذلك إلى أن نصل إلى اللقطة التي تتضمّن الطابع الزمني المطلوب.

ما هي الخطوات التالية؟

نحدّد النطاق على الواجهة الأمامية على أنّه القدرة على الحفاظ على تشغيل دقيق و فعّال مع ازدياد حجم المشاريع وتعقيدها. تتمثل إحدى طرق توسيع الأداء هو تحميل أقل عدد ممكن من الفيديوهات في آنٍ واحد، ولكن عندما فإننا نخاطر بتحولات بطيئة ومتقطعة. وعلى الرغم من تطوير أعمالنا للتخزين المؤقت لمكونات الفيديو لإعادة استخدامها، إلا أن هناك قيودًا على مقدار التحكم في علامات فيديو HTML5 التي يمكن أن تقدمها.

في المستقبل، قد نحاول تشغيل جميع الوسائط باستخدام WebCodecs. يمكن أن يسمح لنا ذلك بتحديد البيانات التي نخزّنها مؤقتًا بدقة، ما يساعد في تحسين الأداء.

يمكننا أيضًا القيام بعمل أفضل من تحميل حوسبة لوحة اللمس الكبيرة إلى عاملي الويب، ويمكننا أن نكون أكثر ذكاءً بشأن عملية الجلب المسبق الملفات والإطارات المُنشأة مسبقًا. نرى فرصًا كبيرة لتحسين أداء التطبيق العام وتوسيع وظائفه باستخدام أدوات مثل WebGL.

نريد مواصلة استثمارنا في TensorFlow.js، الذي نستخدمه حاليًا للقيام بعمليات إزالة الخلفية الذكية. نخطط للاستفادة من TensorFlow.js في مهام متقدّمة، مثل رصد العناصر واستخراج الميزات ونقل النمط وما إلى ذلك

في النهاية، نحن متحمّسون لمواصلة تطوير منتجنا الذي يقدّم أداءً ووظائف مماثلة للتطبيقات الأصلية على الويب المفتوح والمجاني.