Другое

Исправление ReferenceError: HTMLElement в FFmpeg WebAssembly приложениях Quasar

Решение ошибки 'ReferenceError: HTMLElement is not defined' при загрузке ffmpeg в WebAssembly в приложениях Quasar SSR с помощью правильных методов загрузки на стороне клиента.

Как исправить ошибку “ReferenceError: HTMLElement is не определен” при загрузке ffmpeg в WebAssembly в приложении Quasar?

Я создаю приложение Quasar и пытаюсь обрабатывать видео с помощью ffmpeg в WebAssembly на стороне клиента. Я установил библиотеку и настроил всё необходимое, но при запуске ffmpeg.load() я столкнулся со следующей ошибкой:

ReferenceError: HTMLElement is not defined client:116:1
 https://<my_url>:9443/@vite/client:116

Вот мой текущий код для загрузки ffmpeg:

javascript
if (!ffmpeg) {
      const mod = await import('@ffmpeg/ffmpeg');
      const FFmpegCtor = (mod as any).FFmpeg;
      ffmpeg = new FFmpegCtor();
      ffmpeg.on('log', ({ message }: { message: string }) => console.log('[ffmpeg]', message));
}

if (!ffmpegUtils) {
    const utilMod = await import('@ffmpeg/util');
    ffmpegUtils = {
        fetchFile: utilMod.fetchFile,
        toBlobURL: utilMod.toBlobURL,
    };
}

// Загрузка ядра FFmpeg из локальных публичных ресурсов и использование выделенного локального воркера
const baseURL = '/ffmpeg';

const coreURL = await ffmpegUtils.toBlobURL(
      `${baseURL}/ffmpeg-core.js`,
      'text/javascript'
);
const wasmURL = await ffmpegUtils.toBlobURL(
      `${baseURL}/ffmpeg-core.wasm`,
      'application/wasm'
    );
const workerURL = await ffmpegUtils.toBlobURL(
      `${baseURL}/worker.js`,
      'text/javascript'
    );

console.log('ffmpeg loading');

await ffmpeg.load({ coreURL, wasmURL, workerURL });

Я уже попробовал следующие шаги по устранению неполадок:

  1. Запуск без workerURL - та же проблема
  2. Получение файлов WASM и JS с CDN или локально - тот же результат
  3. Обновление tsconfig с:
    json
    "compilerOptions": {
      "baseUrl": ".",
      "lib": ["dom"]
    }
    
  4. Обновление quasar.config.js с:
    javascript
    viteConf.optimizeDeps = viteConf.optimizeDeps || {};
    const excluded = new Set([
        ...(viteConf.optimizeDeps.exclude || []),
            '@ffmpeg/ffmpeg',
            '@ffmpeg/util',
            '@ffmpeg/core',
            '@ffmpeg/core-mt',
            '@ffmpeg/core-st',
        ]);
    viteConf.optimizeDeps.exclude = Array.from(excluded);
    

Некоторые ИИ-агенты предполагают, что это может быть связано с тем, что процесс выполняется в воркере, у которого нет доступа к элементам DOM. Как я могу решить эту проблему и успешно загрузить ffmpeg в WebAssembly в моем приложении Quasar?

Ошибка “ReferenceError: HTMLElement is not defined” возникает из-за того, что ffmpeg.wasm пытается получить доступ к элементам DOM во время инициализации, но это происходит в среде без браузерских API — вероятно, в контексте Quasar SSR или в потоке worker. Для решения этой проблемы необходимо убедиться, что ffmpeg.wasm загружается только в браузерной среде.

Содержание

Понимание ошибки

Ошибка возникает из-за того, что ffmpeg.wasm пытается получить доступ к браузерным API таким, как HTMLElement, во время инициализации. В приложениях Quasar с серверным рендерингом (SSR) процесс серверного рендеринга не имеет доступа к этим браузерным API, и если ffmpeg загружается до гидратации на клиенте, произойдет сбой.

Документация Quasar SSR объясняет, что SSR-компоненты создают и манипулируют DOM в браузере, но во время серверного рендеринга эти API недоступны.

Решения для приложений Quasar SSR

1. Условная загрузка на стороне клиента

Наиболее надежное решение — убедиться, что ffmpeg загружается только в браузерной среде:

javascript
let ffmpeg = null;
let ffmpegUtils = null;

export const loadFFmpeg = async () => {
  // Проверяем, находимся ли мы в браузерной среде
  if (typeof window === 'undefined') {
    console.log('Пропускаем загрузку FFmpeg - не в браузерной среде');
    return;
  }

  if (!ffmpeg) {
    const mod = await import('@ffmpeg/ffmpeg');
    const FFmpegCtor = (mod as any).FFmpeg;
    ffmpeg = new FFmpegCtor();
    ffmpeg.on('log', ({ message }: { message: string }) => console.log('[ffmpeg]', message));
  }

  if (!ffmpegUtils) {
    const utilMod = await import('@ffmpeg/util');
    ffmpegUtils = {
      fetchFile: utilMod.fetchFile,
      toBlobURL: utilMod.toBlobURL,
    };
  }

  const baseURL = '/ffmpeg';
  const coreURL = await ffmpegUtils.toBlobURL(
    `${baseURL}/ffmpeg-core.js`,
    'text/javascript'
  );
  const wasmURL = await ffmpegUtils.toBlobURL(
    `${baseURL}/ffmpeg-core.wasm`,
    'application/wasm'
  );
  const workerURL = await ffmpegUtils.toBlobURL(
    `${baseURL}/worker.js`,
    'text/javascript'
  );

  console.log('загрузка ffmpeg');
  await ffmpeg.load({ coreURL, wasmURL, workerURL });
};

2. Использование хуков жизненного цикла Quasar

В вашем Vue-компоненте используйте хуки жизненного цикла Quasar для обеспечения загрузки только на клиенте:

javascript
import { onMounted } from 'vue';

export default {
  setup() {
    const ffmpeg = ref(null);
    
    onMounted(async () => {
      await loadFFmpeg();
    });
    
    return { ffmpeg };
  }
}

3. Динамический импорт с проверкой SSR

Если вам нужно загрузить ffmpeg в компоненте, который может быть отрендерен на сервере:

javascript
<script setup>
import { ref, onMounted } from 'vue';

const ffmpeg = ref(null);
const isLoading = ref(false);
const error = ref(null);

const loadFFmpeg = async () => {
  if (typeof window === 'undefined') return;
  
  isLoading.value = true;
  try {
    const mod = await import('@ffmpeg/ffmpeg');
    const utilMod = await import('@ffmpeg/util');
    
    const FFmpegCtor = mod.FFmpeg;
    ffmpeg.value = new FFmpegCtor();
    
    ffmpeg.value.on('log', ({ message }) => {
      console.log('[ffmpeg]', message);
    });
    
    const baseURL = '/ffmpeg';
    const coreURL = await utilMod.toBlobURL(
      `${baseURL}/ffmpeg-core.js`,
      'text/javascript'
    );
    const wasmURL = await utilMod.toBlobURL(
      `${baseURL}/ffmpeg-core.wasm`,
      'application/wasm'
    );
    
    await ffmpeg.value.load({ coreURL, wasmURL });
  } catch (err) {
    error.value = err;
    console.error('Не удалось загрузить FFmpeg:', err);
  } finally {
    isLoading.value = false;
  }
};

onMounted(loadFFmpeg);
</script>

Конфигурация Worker и альтернативы

1. Конфигурация без Worker

Если проблема связана с потоками worker, попробуйте загрузить ffmpeg без использования workers:

javascript
await ffmpeg.load({
  coreURL,
  wasmURL,
  // Удалите workerURL для использования inline worker
});

2. Решение с предсобранным Worker

Создайте отдельный файл worker и правильно ссылайтесь на него:

javascript
// worker.js
self.importScripts('/ffmpeg/ffmpeg-core.js');

// В основном коде
const workerURL = URL.createObjectURL(
  new Blob([`
    importScripts('${baseURL}/ffmpeg-core.js');
  `], { type: 'application/javascript' })
);

3. Загрузка на основе CDN

Рассмотрите возможность использования CDN-версии, которая может иметь лучшую совместимость с браузерами:

javascript
const coreURL = 'https://unpkg.com/@ffmpeg/core@0.12.4/dist/ffmpeg-core.js';
const wasmURL = 'https://unpkg.com/@ffmpeg/core@0.12.4/dist/ffmpeg-core.wasm';

Полный пример реализации

Вот полное решение для приложения Quasar SSR:

javascript
// utils/ffmpeg.js
import { ref } from 'vue';

// Реактивное состояние
export const ffmpegReady = ref(false);
export const ffmpegError = ref(null);

let ffmpegInstance = null;
let ffmpegUtils = null;

export const getFFmpeg = () => {
  if (!ffmpegReady.value) {
    throw new Error('FFmpeg не готов. Сначала вызовите loadFFmpeg().');
  }
  return ffmpegInstance;
};

export const loadFFmpeg = async () => {
  // Пропускаем на сервере
  if (typeof window === 'undefined') {
    console.log('Пропускаем загрузку FFmpeg - серверная сторона');
    return;
  }

  if (ffmpegReady.value) {
    return ffmpegInstance;
  }

  try {
    // Динамические импорты
    const mod = await import('@ffmpeg/ffmpeg');
    const utilMod = await import('@ffmpeg/util');
    
    // Создание экземпляра FFmpeg
    ffmpegInstance = new mod.FFmpeg({
      log: true,
    });
    
    ffmpegInstance.on('log', ({ message }) => {
      console.log('[ffmpeg]', message);
    });
    
    // Сохранение утилит
    ffmpegUtils = {
      fetchFile: utilMod.fetchFile,
      toBlobURL: utilMod.toBlobURL,
    };
    
    // Настройка URL
    const baseURL = '/ffmpeg';
    const coreURL = await ffmpegUtils.toBlobURL(
      `${baseURL}/ffmpeg-core.js`,
      'text/javascript'
    );
    const wasmURL = await ffmpegUtils.toBlobURL(
      `${baseURL}/ffmpeg-core.wasm`,
      'application/wasm'
    );
    
    // Загрузка FFmpeg
    await ffmpegInstance.load({
      coreURL,
      wasmURL,
    });
    
    ffmpegReady.value = true;
    console.log('FFmpeg успешно загружен');
    
  } catch (err) {
    ffmpegError.value = err;
    console.error('Не удалось загрузить FFmpeg:', err);
    throw err;
  }
};

// Функция очистки
export const cleanupFFmpeg = () => {
  if (ffmpegInstance) {
    ffmpegInstance = null;
    ffmpegUtils = null;
    ffmpegReady.value = false;
  }
};

И в вашем компоненте:

javascript
<template>
  <div>
    <q-btn 
      :loading="!ffmpegReady" 
      :disable="!ffmpegReady"
      @click="processVideo"
    >
      Обработать видео
    </q-btn>
    
    <div v-if="ffmpegError" class="text-negative">
      Ошибка загрузки FFmpeg: {{ ffmpegError.message }}
    </div>
  </div>
</template>

<script setup>
import { onMounted, onBeforeUnmount } from 'vue';
import { loadFFmpeg, getFFmpeg, ffmpegReady, ffmpegError, cleanupFFmpeg } from 'utils/ffmpeg';

onMounted(async () => {
  await loadFFmpeg();
});

onBeforeUnmount(() => {
  cleanupFFmpeg();
});

const processVideo = async () => {
  try {
    const ffmpeg = getFFmpeg();
    // Ваш код обработки видео здесь
    console.log('Обработка видео...');
  } catch (err) {
    console.error('Обработка видео не удалась:', err);
  }
};
</script>

Дополнительные шаги по устранению неполадок

1. Обновление конфигурации Vite

Обновите ваш quasar.config.js для правильной обработки зависимостей ffmpeg:

javascript
// quasar.config.js
const { configure } = require('quasar/wrappers');

configure(({ ctx }) => {
  return {
    // ... другая конфигурация
    
    vite: {
      optimizeDeps: {
        exclude: [
          '@ffmpeg/ffmpeg',
          '@ffmpeg/util',
          '@ffmpeg/core',
          '@ffmpeg/core-mt',
          '@ffmpeg/core-st'
        ]
      },
      // Добавьте это для правильной обработки worker
      define: {
        global: 'globalThis',
      },
      // Добавьте это для поддержки SharedArrayBuffer при необходимости
      server: {
        headers: {
          'Cross-Origin-Opener-Policy': 'same-origin',
          'Cross-Origin-Embedder-Policy': 'require-corp'
        }
      }
    }
  };
});

2. Конфигурация TypeScript

Убедитесь, что ваш tsconfig.json включает правильные типизации DOM:

json
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "types": ["node"],
    "module": "esnext",
    "target": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

3. Настройка публичных ассетов

Убедитесь, что ваши файлы ffmpeg правильно размещены в публичной директории:

public/
└── ffmpeg/
    ├── ffmpeg-core.js
    ├── ffmpeg-core.wasm
    └── worker.js

4. Переменные окружения

Добавьте переменные окружения для контроля загрузки ffmpeg:

javascript
// .env
VITE_FFmpeg_ENABLED=true
VITE_FFmpeg_DEBUG=true

// .env.ssr
VITE_FFmpeg_ENABLED=false

Затем в вашем коде:

javascript
const shouldLoadFFmpeg = import.meta.env.VITE_FFmpeg_ENABLED !== 'false' && typeof window !== 'undefined';

Источники

  1. Документация Quasar SSR
  2. Проблемы на GitHub FFmpeg.wasm - решения ReferenceError
  3. Поддержка SharedArrayBuffer в Cloudflare
  4. Руководство по устранению неполадок FFmpeg.wasm
  5. Конфигурация Quasar SSR
  6. SSR с WebAssembly
  7. Проблемы загрузки FFmpeg.wasm в Vanilla JavaScript

Заключение

Ошибка “ReferenceError: HTMLElement is not defined” в ffmpeg.wasm в приложениях Quasar в основном вызвана попыткой загрузки кода, зависимого от браузера, в контексте серверного рендеринга. Ключевые решения:

  1. Всегда проверяйте браузерную среду с помощью typeof window !== 'undefined' перед загрузкой ffmpeg
  2. Используйте хуки жизненного цикла Quasar такие как onMounted для обеспечения инициализации на стороне клиента
  3. Правильно настраивайте vite с правильными исключениями и настройками worker
  4. Рассмотрите конфигурацию без worker если проблема сохраняется с потоками worker
  5. Реализуйте правильную обработку ошибок и состояния загрузки для лучшего пользовательского опыта

Следуя этим подходам, вы можете успешно интегрировать ffmpeg.wasm в ваше приложение Quasar SSR, избегая ошибок ссылки на HTMLElement. Ключ заключается в том, чтобы убедиться, что ffmpeg.wasm загружается и выполняется только в браузерной среде, где все необходимые браузерные API доступны.

Авторы
Проверено модерацией
Модерация