Идёте вы, уважаемый читатель, погожим летним вечером по улице, никого не трогаете, и тут на вас наезжают (тьфу-тьфу-тьфу, как говорится). Хулиганы, просто прохожие, специальные товарищи (как пелось в одной старой песенке) не столь важно. Вы достаете телефон и начинаете снимать происходящее на видео. Это не очень нравится наезжающим, и телефон у вас отбирают (или изымают на законных основаниях нужное подчеркнуть). Свидетелей нет, видеозаписи на телефоне больше нет, доказательств для полиции и суда тоже, соответственно, никаких.
Выход из этой ситуации очевиден: видеозапись должна вестись не в локальный файл на ваш телефон, а непосредственно на удаленный сервер. Правда, готовых программных решений для реализации этой идеи не так много (например, вот): в большинстве случаев предлагаемые приложения для мобильного телефона или платные, или работают из рук вон плохо. Экзотические рекомендации типа в случае нападения хулиганов начните трансляцию на 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()">⏺</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;// Забираем пароль из queryStringlet 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 = "⏹"; play(); } else { butt.innerHTML = "⏺"; 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
; - нажать на кнопку Запись на загрузившейся страничке.
Последнее действие можно и исключить, если слегка подправить код фронтеда так, чтобы запись включалась сразу при загрузке страницы.
Вот, собственно и всё: простое решение, доступное каждому. Искренне желаю, чтобы лично вам это никогда не пригодилось...