...

суббота, 13 июня 2020 г.

Видеозапись в облако своими руками

Идёте вы, уважаемый читатель, погожим летним вечером по улице, никого не трогаете, и тут… на вас наезжают (тьфу-тьфу-тьфу, как говорится). Хулиганы, просто прохожие, специальные товарищи (как пелось в одной старой песенке) — не столь важно. Вы достаете телефон и начинаете снимать происходящее на видео. Это не очень нравится наезжающим, и телефон у вас отбирают (или изымают на законных основаниях — нужное подчеркнуть). Свидетелей нет, видеозаписи на телефоне больше нет, доказательств для полиции и суда тоже, соответственно, никаких.

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

Можно, конечно, подучить Java или Kotlin (а заодно и Swift) или, на худой конец, освоить PhoneGap и написать своё приложение. Однако всё гораздо проще: под катом несложное решение этой задачи посредством HTML5 video/audio API.


Связываться ли с WebRTC

Безусловно, WebRTC — очень крутая штука, позволяющая вести трансляцию в облако непосредственно. Однако реализация такой трансляции — тот еще геморрой, поэтому я выбрал решение гораздо проще. Видео пишется в оперативную память телефона (заметьте, не на SD-карту, а только в оперативную память) и каждую минуту (например), а также по завершении записи отправляется на сервер. То есть даже если хулиганы начали отбирать у вас телефон — вы успеваете нажать кнопку «стоп» и последний видеофайл уходит на сервер.

При настройках по умолчанию одна минута записи — это файл размером около 20 МБ. При этом никаких приложений, хоть готовых, хоть самописных — только хардкор, только HTML и javascript.


Проблема кроссбраузерности

Справедливости ради надо сказать, что поддержка HTML5 video/audio API, хоть и развивается стремительно, все еще доставляет массу проблем разработчику. В предлагаемом ниже коде я сознательно не стал приводить кроссбраузерного варианта, чтобы не усложнять восприятие. Я даже, если честно, не тестировал этот код под различными ОС и различными браузерами: всё написанное замечательно работает в Mozilla Firefox 68 из-под Debian и в Chrome 83 из-под Android 7; в Chromium 80 из-под Debian и во многих браузерах для Android уже не работает в том, виде, в котором написано.

Так как вы будете использовать предложенное ниже исключительно в личных целях и на своем (скорее всего, на одном) мобильном телефоне, нужно просто найти реализацию video/audio API, поддерживаемую вашим устройством. Так, использованное мною navigator.mediaDevices.getUserMedia() придется, возможно, заменить на navigator.getUserMedia() или даже на navigator.webkitGetUserMedia, либо на navigator.mozGetUserMedia. Можно, конечно, написать и кроссбраузерный вариант. Кроме того, может потребоваться замена конструкции video.srcObject = stream на video.src = URL.createObjectURL(stream). Наконец, проблемы могут возникнуть из-за отсутствия поддержки MediaRecorder и fetch; последний, впрочем, легко заменяется AJAX'ом.


Итак, приступим… Фронтенд

Как вы уже, наверно, поняли, мы собираемся написать html-страничку, которая берет видеопоток с камеры телефона (или ноутбука, или планшета, или стационарного компьютера) и раз в минуту отправляет соответствующий видеофайл на сервер fetch-запросом.

Html-файл очень прост, если не сказать элементарен:

<!DOCTYPE html>
<html lang="ru">
<head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,
      initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="robots" content="noindex, nofollow">
<link rel="stylesheet" type="text/css" href="style.css">
<title>VideoCamera</title>
</head>
<body>

<video muted></video>
<button type="button" onclick="go()">&#9210;</button>
<script src="main.js"></script>

</body>
</html>

Здесь, собственно, только два элемента: окно, в котором пользователю будет показываться снимаемое им видео (без звука, чтобы не было эффекта эха; при этом на сервер звук будет отправляться, естественно) и кнопка «Запись/Стоп». Для того, чтобы все это красиво выглядело и на телефоне, и на десктопе, пишем нехитрый style.css:

html {
   height: 100%;}
body {
   height: 100%; margin: 0px; padding: 0px; background: black;
   text-align: center;}
video {
   display: block; max-height: 100%; max-width: 100%; margin: auto;}
button {
   display: inline-block; width: 2em; margin-left: -1em;
   position: absolute; bottom: 20px; left: 50%; background: none;
   outline: none; border: none; font-size: 30px;  text-align: center;}

И, наконец, main.js, который выполняет всю работу на фронтенде:

"use strict";

// Длительность одного блока записи в секундах
const recTime = 60;

// Забираем пароль из queryString
let pwd = location.search || 'a'; pwd = pwd.trim().replace('?', '');

const video = document.querySelector("video"),
      butt  = document.querySelector("button");

let media, playFlag = false;

// Начать запись видео
const play = async () => {
   try {
      // Если клиент зашел со смартфона, включаем основную камеру
      let c = /Android|iPhone/i.test(navigator.userAgent) ?
         {video:{facingMode:{exact:"environment"}}, audio:true} :
         {video:true, audio:true};

      // Получаем видеопоток с камеры и показываем его юзеру
      let stream = await navigator.mediaDevices.getUserMedia(c);
      video.srcObject = stream;
      video.play();

      // Пишем видеопоток на сервер каждые recTime секунд
      media = new MediaRecorder(stream);
      media.ondataavailable = d => {
         fetch("api.php", {
            method: "POST",
            headers: {"Content-Type": "video/webm", "X-PWD": pwd},
            body: d.data
         })
      };
      media.start(recTime * 1000);
   }
   catch(err) {alert(err);}
};

// Обработчик нажатия кнопки Запись/Стоп
const go = () => {
   if (!playFlag) {
      butt.innerHTML = "&#9209;";
      play();
   }
   else {
      butt.innerHTML = "&#9210;";
      video.pause();
      video.srcObject = null;
      media.stop();      
   }
   playFlag = !playFlag;
}

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

Это можно сделать различными способами (типа классического получения токена с сервера в ответ на отправленный пароль или анализа fingerprint клиента), но я решил не заморачиваться и поступил гораздо проще: пароль просто передается на сервер в заголовке X-PWD fetch-запроса; при этом пароль не вводится пользователем (вряд ли в глухом переулке у вас будет время для ввода пароля), а просто содержится в query string. Таким образом, для обращения к написанному сервису используется URL типа

https://my_domen/path/?abcde

где abcde и является паролем. На серверной же стороне пароль просто записан в коде: повторюсь, мы пишем это всё для себя, любимого, поэтому, на мой взгляд, можно обойтись таким примитивным способом аутентификации. Параноики могут, конечно, написать что-нибудь более продвинутое.


… а теперь бэкенд

Начнем с проблемы хостинга и https. Реальность, увы, такова, что доступ к видеопотоку с вашей камеры вы не получите, если html-страничка получена по http. Наверно, это правильно. Выхода из этой ситуации, как обычно, два: либо использовать самоподписанный сертификат (вы же один, можно просто однократно принять этот сертификат и больше не заморачиваться), либо найти хостинг с поддержкой https.

Бесплатных хостингов, в том числе с поддержкой https, сейчас достаточно. Лучшим вариантом, конечно, будет хостить проект просто у себя, дома или на работе; не все, однако, хотят с этим связываться, поэтому бэкенд я написал на php, поддержка которого на бесплатных хостингах есть повсеместно. Вы будете смеяться, но файл api.php состоит всего из 6 строк:

<?php
$pwdTrue = "abcde";
$pwd     = $_SERVER["HTTP_X_PWD"];
if ($pwdTrue !== $pwd) exit;

@$data = file_get_contents("php://input") or $data = '';
$flName = date("ymd-His").".webm";

if ($data) file_put_contents("video/".$flName, $data);
?>

Сервер просто принимает пришедший fetch-запросом видеофайл и кладет его в папку video с именем типа 200613-190123.webm (где 13.06.20 — дата, а 19:01:23 — время). При этом папка video будет доступна всем желающим (что довольно удобно, потому что можно скачать записанное видео просто браузером); если вы этого не хотите, можно закрыть эту папку с помощью .htaccess или другим способом, а отснятое видео забирать по ftp.

Здесь необходимо сделать важное замечание. Если ваша неприятная встреча в пустынном переулке длилась, например, 5 с небольшим минут, то на сервер будет отправлено 6 видеофайлов (пять минутных и шестой с оставшимся «хвостиком»). Корректно проигрываться при этом будет только первый; остальные (такова особенность реализации MediaRecorder) будут считаться продолжениями предыдущих и самостоятельно воспроизводиться не будут.

Это, однако, не недостаток, а скорее достоинство: чтобы получить цельную видеозапись, вам не нужно открывать видеоредактор и склеивать кусочки (что само по себе нехорошо, поскольку следы монтажа обнаружит любая судебная экспертиза). Достаточно просто сконкатенировать все файлы в один, и итоговое видео готово (ниже вариант для unix-подобных ОС):

$ cd путь_к_папке_с_файлами
$ cat * > новое_имя.webm

Как пользоваться

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

Понятно, что в экстренной ситуации вы не будете долго открывать браузер и тем более вводить какой-то URL, да еще с паролем в query string. Кроме того, в Chrome для Android нельзя задать стартовую (не путать с домашней!) страницу. Открывать же браузер, а затем нажимать на значок домика (если вы установили написанное в качестве домашней страницы) довольно долго.

Выход очень прост: создаем в файловой системе телефона простенький файлик alarm.html:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=https://domen/path?abcde">
</head>
<body></body>
</html>

Создаем для этого файлика ярлык на рабочем столе телефона (прямо на главном экране). Теперь в экстренной ситуации вам необходимо выполнить всего три действия:


  • включить мобильный интернет (если он не включен у вас на телефоне постоянно);
  • кликнуть на ярлыке alarm.html;
  • нажать на кнопку «Запись» на загрузившейся страничке.

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

Вот, собственно и всё: простое решение, доступное каждому. Искренне желаю, чтобы лично вам это никогда не пригодилось...

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий