Поврежденная загрузка видео при фрагментировании MediaRecorder на платформу Google Cloud

В настоящее время я использую компонент с питанием от React Hook, чтобы записать свой экран и впоследствии загрузить его в Google Cloud Storage. Однако по завершении файл, созданный в Google Cloud, оказывается поврежденным.

Это суть кода в моем компоненте React, отсюда useMediaRecorder: https://github.com/wmik/use-media-recorder -

let {
    error,
    status,
    mediaBlob,
    stopRecording,
    getMediaStream,
    startRecording,
    liveStream,
  } = useMediaRecorder({
    onCancelScreenShare: () => {
      stopRecording();
    },
    onDataAvailable: (chunk) => {
      // do the uploading here:
      onChunk(chunk);
    },
    recordScreen: true,
    blobOptions: { type: "video/webm;codecs=vp8,opus" },
    mediaStreamConstraints: { audio: audioEnabled, video: true },
  });

Когда данные становятся доступными через этот хук - он вызывает onChunk (chunk), передающий двоичный Blob этому методу, чтобы выполнить загрузку, я связываюсь с этим разделом кода для выполнения загрузки:

 const onChunk = (binaryData) => {
    var formData = new FormData();
    formData.append("data", binaryData);
    let customerApi = new CustomerVideoApi();
    customerApi.uploadRecording(
      videoUUID,
      formData,
      (res) => {},
      (err) => {}
    );
  };

customerApi.uploadRecording выглядит так (с использованием аксиомов).

const uploadRecording = (uuid, data, fn, fnErr) => {
    axios
      .post(endpoint + "/stream/upload", data, {
        headers: {
          "Content-Type": "multipart/form-data",
        },
      })
      .then(function (response) {
        fn(response);
      })
      .catch(function (error) {
        fnErr(error.response);
      });
  };

HTTP-запрос выполнен успешно, и с миром все в порядке: код на стороне сервера для загрузки основан на laravel:

// это внутри контроллера.

 public function index( Request $request )
    {

            // Set file attributes.
            $filepath = '/public/chunks/';
            $file = $request->file('data');
            $filename = $uuid . ".webm"; 
          
            // streamupload
            File::streamUpload($filepath, $filename, $file, true);

            return response()->json(['uploaded' => true,'uuid'=>$uuid]);
      
    }

// есть поставщик услуг, используемый для создания нового макроса для объекта File ::, предоставляющий средства для соответствующей обработки потока:

public function boot()
    {
        File::macro('streamUpload', function($path, $fileName, $file, $overWrite = true) {
            
            $resource = fopen($file->getRealPath(), 'r+');
           
            $storageClient = new StorageClient([
                'projectId' => 'myprjectid',
                'keyFilePath' => '/my/path/to/servicejson.json',
            ]);
            
            $bucket = $storageClient->bucket('mybucket');
            $adapter = new GoogleStorageAdapter($storageClient, $bucket);
            $filesystem = new Filesystem($adapter);

            return $overWrite 
                    ? $filesystem->putStream($fileName, $resource) 
                    : $filesystem->writeStream($fileName, $resource);
        });
    }

Итак, повторим:

  1. Приложение React выделяет капли,
  2. сторона сервера определяет, должен ли он быть создан или добавлен в Google Cloud Storage.
  3. на стороне сервера успешно.

4) Видео внутри платформы Google Cloud повреждено.

Однако видеофайл в контейнере Google Cloud поврежден и не воспроизводится. Я не уверен, почему он поврежден, но пока мои догадки:

  1. Какая-то проблема типа Dodgy Mime ... - разные браузеры, похоже, обрабатывают кодек / тип файла иначе, чем медиа-рекордер: например, Кажется, что Chrome - это x-matroska (.mkv?) - firefox снова другой ... В идеале у меня был бы контейнер .webm - обратите внимание, как я устанавливаю серверную сторону имени файла, а она не исходит от клиента. Должен ли он? Я не уверен, как заставить MediaRecorder быть конкретным mimeType - я думал, что параметр blobOptions должен сделать это, но изменение расширения и типа mime, похоже, практически не влияет на возникновение повреждения.

  2. Некоторая проблема во время загрузки, когда HTTP-запрос не выполняется и не завершается по порядку - например,

1 onDataAvailable completes second
2 onDataAvailable completes first
3 onDataAvailable completes third

Я как бы исключил это, потому что считаю, что куски должны быть достаточно маленькими.

  1. Какая-то проблема с API-интерфейсами Google Cloud Storage, которые я использую, возможно, не так? Поддерживает ли облачная платформа потоковую передачу и отправляет ли эта библиотека правильные параметры для этого?

  2. Какая-то проблема с тем, как я загружаю - должны ли заголовки axios быть составными данными формы или чем-то еще?

Это пакет, который я использую на стороне сервера: https://github.com/Superbalist/flysystem-google-cloud-storage

Может ли кто-нибудь пролить свет на то, как достичь этой цели потоковой передачи в Google Cloud без повреждения видео с медиа-рекордера? Надеюсь, в этом вопросе достаточно подробностей, чтобы понять это. Проблема, как показано на рисунке, заключается не в том, чтобы доставить файл до облака Google, а в том, что полученный файл не может быть воспроизведен в любом видеоформате.

Обновить

Я заказал свои фрагменты на стороне клиента и правильно поставил их в очередь, прежде чем позволить им добраться до сервера. Никакой разницы на выходе. Как некоторые предположили, один запрос на загрузку большого двоичного объекта работает нормально.

Пробовал использовать параметр потоковой конфигурации (из чтения исходного кода кажется, что куски должны быть определенного размера, прежде чем Google распознает их как возобновляемую загрузку

$ filesystem = новая файловая система ($ adapter, ['resumable' = ›true]);

Не знаю, как: https://cloud.google.com/storage/docs/performing-resumable-uploads - реализовано в библиотеках, которые я использую (или в самих API Google Cloud, если вообще?). Мне нужно реализовать это самому? Документация со стороны Google невелика.


person Squiggs.    schedule 06.01.2021    source источник
comment
@JanosVinceller - это спам? У меня нет среднего членства, и я не собираюсь его покупать.   -  person Squiggs.    schedule 15.01.2021


Ответы (2)


Краткая версия: первое, что вам нужно сделать, это локально выполнить буферизацию всего видео и отправить одну полезную нагрузку на сервер и на диск Google. Это подтвердит, что ваш код для небольшого видео действительно правильный. Убедившись в этом, можно переходить к загрузке нескольких фрагментов.

Более длинная версия: для начала, вы не передаете uuid в запрос, он используется:

const uploadRecording = (uuid, data, fn, fnErr) => {
    axios
      .post(endpoint + "/stream/upload", data, {
        headers: {
          "Content-Type": "multipart/form-data",
        },
      })
      .then(function (response) {
        fn(response);
      })
      .catch(function (error) {
        fnErr(error.response);
      });
  };

Далее, вы не можете доверять тому, как будет работать разбиение на фрагменты, я думаю, вы проверили это поведение с неупорядоченным результатом ведения журнала фрагментов. Вы должны предположить, что на вашем сервере вы будете получать фрагменты не по порядку и правильно их обработать.

Каждый фрагмент, который вы получаете на сервере, необходимо поместить в нужное место, вы не можете просто написать поток, вам нужно записать в явный двоичный блок. В частности, в каждом запросе указывайте диапазон байтов: Google docs :

curl -i -X PUT --data-binary @CHUNK_LOCATION \
    -H "Content-Length: CHUNK_SIZE" \
    -H "Content-Range: bytes CHUNK_FIRST_BYTE-CHUNK_LAST_BYTE/TOTAL_OBJECT_SIZE" \
    "SESSION_URI"
  • CHUNK_LOCATION - это локальный путь к фрагменту, который вы загружаете в данный момент.
  • CHUNK_SIZE - это количество байтов, которые вы загружаете в текущем запросе. Например, 524288.
  • CHUNK_FIRST_BYTE - это начальный байт в общем объекте, который содержит загружаемый фрагмент.
  • CHUNK_LAST_BYTE - это конечный байт в общем объекте, который содержит загружаемый фрагмент.
  • TOTAL_OBJECT_SIZE - это общий размер загружаемого объекта.
  • SESSION_URI - это значение, возвращаемое в заголовке Location, когда вы инициировали возобновляемую загрузку.
person Warren Parad    schedule 09.01.2021

Постарайтесь исключить как можно больше переменных и точно определить, где именно поврежден файл.

Поскольку вы используете путь React (JS) - ›Laravel (PHP) -› GoogleCloud, первое, что я предлагаю, - это протестировать каждый шаг отдельно:

  • React - ›Laravel - сохраните файл на своем сервере и проверьте, не поврежден ли он на данный момент.
  • Laravel - ›GoogleCloud - Загрузите файл из файловой системы сервера и загрузите его в облако и посмотрите, не поврежден ли он.

У меня нет опыта работы с облаком Google, но я сделал что-то очень похожее с AWS и обнаружил, что их служба загрузки видео очень требовательна к запросам (включая порядок отправляемых заголовков). Попытайтесь сравнить спецификации используемой вами службы с вашими входными данными, сделайте наименьшее возможное, что работает, и начните добавлять переменные, пока не дойдете до конечного состояния.

Также я не вижу в вашем коде какого-либо упорядочивания данных. Если ваши фрагменты расположены близко друг к другу, и при потоковой передаче это очень возможно, то есть вероятность, что они будут доставлены в другом порядке, чем первоначально отправленные. Если вы просто добавите их в файл без какого-либо контроля над сортировкой, файл действительно будет поврежден. Не уверен, что для webm это приведет к поломке только части видео или к смерти всего объекта.

person HubertNNN    schedule 09.01.2021