Записываем экран и звук через расширение в браузере и сохраняем в NextCloud

от автора

Здравствуйте дорогие читатели.

В статье делюсь опытом создания расширения для Chromium и Google Chrome браузера.

Раньше я пользовался «условно бесплатными расширениями и программами для записи скринкастов», но в какой-то момент некоторые из них стали платными, и их удобства сошли на “нет”. А в некоторых оставались вопросы к безопасности данных и сложности с оплатой. К тому же, я не нашёл программ или расширений с функциями сохранения в своём облаке или сервере.

Возможно вы скажите — зачем мне расширение для браузера?! Ведь я могу взять ffmpeg с x11grab, приправить всё это bash-скриптом с использованием curl, и отправлять результаты в облако одной лишь командой в терминале! И возможно быстренько «перенесу» это решение под все операционные системы! И вы будете правы, но решение получится сложным. А если у нас есть под рукой браузер, то воспользуемся его возможностями (да, это странно — браузер для просмотра HTML-страничек, который записывает ваш экран).

Ссылка на готовое решение под катом.

Статья разделена на 2 части: первая часть относится к extensions API и MediaStream API, вторая к интеграции в Nextcloud.

В статье есть отсылки к исходному коду, поэтому предлагаю во время чтения подсматривать в исходный код.

Часть первая

Первое с чего я начал: оценка возможности создания такого расширения и возможность собрать минимально-рабочую версию. Логично было сразу зайти в документацию по extensions API для Chrome и проверить есть ли что-то про «desktop capture» — и такое там есть.

Далее нужно было изучить desktopCapture API и проверить как это работает. Там же был найден пример кода и расширения для записи содержимого вкладки, с замечанием на то, что код можно адаптировать для записи экрана.

Было требование, чтобы основной “код записи видеопотока” выполнялся в «background», но работа с offscreen api имеет ограничения. Например, в момент начала записи нужно запросить разрешение на экран и на микрофон (если это запись экрана со звуком) . Через offscreen api не получилось сделать, поэтому использовался другой подход: перед записью экрана будет открываться вкладка, в которой будут запрашиваться разрешения. И эта вкладка будет открыта во время записи, но на ней не будет фокусировки. (возможно следовало оставить фокусировку на этой вкладке, чтобы предоставить более детальную информацию о процессе записи).

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

Файл manifest.json

{   "name": "__MSG_extName__",   "description": "__MSG_extDesc__",   "version": "1.0",   "manifest_version": 3,   "default_locale": "ru",   "icons": {     "16": "images/icon-16.png",     "32": "images/icon-32.png",     "48": "images/icon-48.png",     "128": "images/icon-128.png"   },   "action":{     "16": "images/icon-16.png",     "32": "images/icon-32.png",     "default_popup":"popup.html"   },   "background": {     "service_worker": "background.js"   },   "permissions": [     "tabs",     "activeTab",     "desktopCapture",     "storage",     "unlimitedStorage"   ],   "host_permissions": [     "*://*/*"   ] }

Здесь service_worker служит для перенаправления сообщений во вкладку записи. Можно сказать что background.js это «маршрутизатор сообщений». Он инициирует запуск вкладки recorder.html через сообщение chrome.tabs.sendMessage(tabId) .

Внутри recorder.html (через recorder.js) мы запрашиваем разрешение на микрофон (если это запись со звуком) и выбираем экран для записи (если у вас подключено несколько экранов) и там же подписываемся на входящие сообщения от background.js и выполняем в нём основной код.

Теперь при закрытии popup.html запись будет продолжаться, а для индикации состояния записи, используем подсветку и вывод текста методами.

chrome.action.setBadgeText({text:  chrome.i18n.getMessage('rec')}); chrome.action.setBadgeBackgroundColor({color: '#eb1e3e'}); chrome.action.setTitle({title: chrome.i18n.getMessage('recording')});

После просмотра видеофайла, оказалось что его нельзя «проматывать». Файл не проматывался, не показывал длительность и невозможно было выполнить перемотку. После поиска в интернете, удалось найти задачу в ней обозначается то, что MediaRecorder не поддерживает и не собирается поддерживать мета информацию → https://github.com/w3c/mediacapture-record/issues/119 что привело к комментарию-решению использовать ts-ebml библиотеку.

Что такое ts-ebml? Это форк библиотеки, которая позволяет работать с медиа-контейнерами webm или mkv. Записывать и читать метаданные.

Далее я начал экспериментировать с этой библиотекой. Клонировал её и собрал в файл EBML.js , чтобы встроить в расширение и получить «проматываемый видеофайл» (seekable video). Для этого проанализировал код примера и применил внутри расширения ( без модификации примера)… И ничего не сработало.

Файл не проматывался. Пришлось изучать исходные коды библиотеки, а также искать ошибки в примере или в «данных» для примера (например слишком короткий тестовый видеофайл примера мог быть неподходящим). В итоге сработал ручной сбор длительности через API EMBL.Reader:

mediaRecorder.onstop = async function(e) {     mediaStream.getTracks().forEach(track => track.stop());     const blobFileSource = new Blob(chunks, {type: nextCloud.getVideoMime()});     const webMBuf = await fetch(URL.createObjectURL(blobFileSource)).then(res=> res.arrayBuffer());     const decoder = new EBML.Decoder();     const reader = new EBML.Reader();     reader.drop_default_duration = false;     var last_duration = 0;     // Этот важный код отсутсвует в оригинальном примере     reader.addListener("duration", ({timecodeScale, duration})=>{         last_duration += duration;     });      const elms = decoder.decode(webMBuf);     elms.forEach((elm)=>{ reader.read(elm); });     reader.stop();      const refinedMetadataBuf = EBML.tools.makeMetadataSeekable(reader.metadatas, last_duration, reader.cues);     const body = webMBuf.slice(reader.metadataSize);      const blobFile = new Blob([refinedMetadataBuf, body], {type: nextCloud.getVideoMime()});     const url = URL.createObjectURL(blobFile);             //... }

Теперь файл проматывается и можно увидеть его длительность в плеере. На этом этапе оценка возможности выполнена. Стало понятно — это работает, а значит теперь результаты можно загружать в «облако» (под облаком подразумевается Nextcloud).

Часть вторая

Для облака использую Nextcloud, поэтому первым делом проанализировал возможности для интеграции, и первым доступным способом стал «протокол WebDAV». У Nextcloud есть документация по работе с WebDAV. В этой документации есть примеры работы для Javascript.

Далее нужно было разобраться с загрузкой файла. Документация требует путь до папки для загружаемого файла. Поэтому в расширении был добавлен раздел «Настройки» , в котором можно настроить доступы к Nextcloud и путь для папки «загружаемых файлов».

Пароль приложения к Nextcloud создаётся в разделе Безопасность.

Пароль приложения к Nextcloud создаётся в разделе Безопасность.

Пользователю потребуется ввести «Адрес сервера» Nextcloud без «trailing slash» ( например https://example.ru ), а в поле «Директория» нужно ввести путь до папки куда будут сохраняться видеофайлы (например «/screencasts». или «/». ). Далее логин и пароль приложения, который создаётся в разделе «Безопасность». Этот логин и пароль, будет передаваться в заголовке запроса Authorization: Basic base64encode() .

Если вам понадобится сделать форк нашего расширения и привязать свой сервис и свои настройки, то рекомендуем использовать раздел «Настроек». Код который отвечает за эту форму находится в функции «_bindSettingsForm» в файле popup.js

Настройки сохраняются через chrome.storage.sync , поэтому они могут автоматически переносится на другие устройства.

Если настройки заполнены, то расширение будет отправлять запросы через «fetch api» к указанному в настройках серверу, и видеофайл будет автоматически загружаться в Nextcloud, а затем выдавать ссылку на видеоплеер (просмотр файла).

Если во время загрузки возникает ошибка, то расширение предложит сохранить файл локально.

Загрузка файла разделена на 2 этапа: первый этап выполнение PUT запроса к webdav адресу будущего файла (смотрите метод uploadFile в recorder.js).

uploadFile: async function (blob, fileName) {       let url = this.upload_path(fileName);        const totalBytes = blob.size;       let bytesUploaded = 0;        // Этот код нужен для отслеживания прогресса загрузки данных       const progressTrackingStream = new TransformStream({           transform(chunk, controller) {               controller.enqueue(chunk);               bytesUploaded += chunk.byteLength;               chrome.runtime.sendMessage({ name: 'nextCloudUploadProgress', data: bytesUploaded / totalBytes });           },           flush(controller) {               console.log("completed stream");           },       });       return await fetch(url, {           method: "PUT",           headers: {               "Content-Type": "application/octet-stream",               "Authorization": this.createAuthHeaderValue()           },           body: blob.stream().pipeThrough(progressTrackingStream),           duplex: "half",       }); }

Данной код, позволяет в процессе загрузки файла, выводить индикатор прогресса.

Второй этап — получение информации о загруженных файлах (смотрите метод fetchVideoPlayerLink в recorder.js).

fetchVideoPlayerLink: async function (fileName) {         const propertyRequestBody = `<?xml version="1.0"?> <d:propfind  xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">   <d:prop>         <oc:fileid />         <d:getlastmodified />   </d:prop> </d:propfind>`;          return new Promise((resolve, reject) => {             fetch(this.base_uri(), {                 method: 'PROPFIND',                 headers: {                     "Accept": "text/plain",                     "Depth": 1,                     "Content-Type": "application/xml",                     "Authorization": this.createAuthHeaderValue()                 },                 body: propertyRequestBody             }).then((response) => {                 response.text().then(text => {                     if (response.status < 400) {                         const nodes = this.parseWebDavFileListXML(text, nextCloud.base_uri(fileName));                         let lastNode = nodes.pop();                         if(lastNode && lastNode.hasOwnProperty('fileid'))                         {                             let playerLink = nextCloud.createPlayerLink(lastNode.fileid);                             resolve(playerLink);                         }                         else                         {                             resolve(null);                         }                     } else {                         reject(new Error({ response }), text)                     }                 });             }, (reason => {                 chrome.runtime.sendMessage({ name: 'nextCloudUploadFileError', data: 'fetch_link_error' });                 console.error('fetch_link_error');             }));         });  }

В этом коде есть недостаток: каждая загрузка файла вытягивает список ранее загруженных. К сожалению, я не разобрался (или не нашёл) способа получения только 1 актуального файла. Если кто-то сталкивался с такой задачей (ограничение кол-ва файлов и сортировка по дате модификации файла в Nextcloud) буду признателен за совет.

Выводы

Если у вас современный Chrome или Chromium (например 119 версии), то с его помощью можно записывать видео с экрана и отправлять его на ваш «собственный» сервер.

Благодарю за внимание и хорошего Вам настроения!


ссылка на оригинал статьи https://habr.com/ru/articles/785850/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *