Идёте вы, уважаемый читатель, погожим летним вечером по улице, никого не трогаете, и тут… на вас наезжают (тьфу-тьфу-тьфу, как говорится). Хулиганы, просто прохожие, специальные товарищи (как пелось в одной старой песенке) — не столь важно. Вы достаете телефон и начинаете снимать происходящее на видео. Это не очень нравится наезжающим, и телефон у вас отбирают (или изымают на законных основаниях — нужное подчеркнуть). Свидетелей нет, видеозаписи на телефоне больше нет, доказательств для полиции и суда тоже, соответственно, никаких.
Выход из этой ситуации очевиден: видеозапись должна вестись не в локальный файл на ваш телефон, а непосредственно на удаленный сервер. Правда, готовых программных решений для реализации этой идеи не так много (например, вот): в большинстве случаев предлагаемые приложения для мобильного телефона или платные, или работают из рук вон плохо. Экзотические рекомендации типа «в случае нападения хулиганов начните трансляцию на 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;
// Забираем пароль из 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 = "⏹";
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
; - нажать на кнопку «Запись» на загрузившейся страничке.
Последнее действие можно и исключить, если слегка подправить код фронтеда так, чтобы запись включалась сразу при загрузке страницы.
Вот, собственно и всё: простое решение, доступное каждому. Искренне желаю, чтобы лично вам это никогда не пригодилось...
denisgrim
А если повредится один фрагмент, то все видео накроется медным тазом?
amg0461 Автор
Я, если честно, не специалист в видеоформатах, в том числе в webm, который большинство браузеров использует по умолчанию. Надо провести эксперимент, тогда и увидим. Писать можно, кстати, в разные контейнеры и с разыми кодеками, это все настраивается.