Так что я добавлю свою шляпу в этот вопрос, так как я придумал новое решение. У меня есть Прогрессивное веб-приложение, которое позволяет пользователям снимать фотографии и видео и загружать их. Мы используем WebRTC, когда это возможно, но используем устройства выбора файлов HTML5 для устройств с меньшей поддержкой * кашель Safari * кашель. Если вы работаете специально над мобильным веб-приложением на базе Android / iOS, которое использует встроенную камеру для непосредственного захвата фотографий / видео, то это лучшее решение, с которым я столкнулся.
Суть этой проблемы в том, что при загрузке страницы file
равен null
, но затем, когда пользователь открывает диалоговое окно и нажимает «Отмена», file
по-прежнему null
, следовательно, он не «изменить», поэтому событие «изменение» не инициируется. Для настольных компьютеров это не так уж и плохо, потому что большинство пользовательских интерфейсов не зависят от знания, когда вызывается отмена, но мобильные пользовательские интерфейсы, которые поднимают камеру для захвата фото / видео, очень зависят от знания когда нажата отмена.
Первоначально я использовал событие document.body.onfocus
, чтобы определить, когда пользователь вернулся из средства выбора файлов, и это работало для большинства устройств, но iOS 11.3 прервала его, поскольку это событие не вызывается.
Концепция
Моё решение - это * вздрогнуть *, чтобы измерить тактовую частоту процессора, чтобы определить, находится ли страница в данный момент на переднем плане или на заднем плане. На мобильных устройствах время обработки отводится приложению, находящемуся на переднем плане. Когда камера видна, она крадет процессорное время и депортирует браузер. Все, что нам нужно сделать, это измерить, сколько времени отводится нашей странице, когда камера запускается, наше доступное время резко сократится. Когда камера отключается (отменяется или иным образом), наше доступное время снова увеличивается.
Осуществление
Мы можем измерить тактирование процессора, используя setTimeout()
, чтобы вызвать обратный вызов за X миллисекунд, а затем измерить, сколько времени потребовалось для его фактического вызова. Браузер никогда не вызовет его точно через X миллисекунд, но если это будет разумно близко, то мы должны быть на переднем плане. Если браузер очень далеко (более чем в 10 раз медленнее, чем запрошено), то мы должны быть в фоновом режиме. Базовая реализация этого выглядит так:
function waitForCameraDismiss() {
const REQUESTED_DELAY_MS = 25;
const ALLOWED_MARGIN_OF_ERROR_MS = 25;
const MAX_REASONABLE_DELAY_MS =
REQUESTED_DELAY_MS + ALLOWED_MARGIN_OF_ERROR_MS;
const MAX_TRIALS_TO_RECORD = 10;
const triggerDelays = [];
let lastTriggerTime = Date.now();
return new Promise((resolve) => {
const evtTimer = () => {
// Add the time since the last run
const now = Date.now();
triggerDelays.push(now - lastTriggerTime);
lastTriggerTime = now;
// Wait until we have enough trials before interpreting them.
if (triggerDelays.length < MAX_TRIALS_TO_RECORD) {
window.setTimeout(evtTimer, REQUESTED_DELAY_MS);
return;
}
// Only maintain the last few event delays as trials so as not
// to penalize a long time in the camera and to avoid exploding
// memory.
if (triggerDelays.length > MAX_TRIALS_TO_RECORD) {
triggerDelays.shift();
}
// Compute the average of all trials. If it is outside the
// acceptable margin of error, then the user must have the
// camera open. If it is within the margin of error, then the
// user must have dismissed the camera and returned to the page.
const averageDelay =
triggerDelays.reduce((l, r) => l + r) / triggerDelays.length
if (averageDelay < MAX_REASONABLE_DELAY_MS) {
// Beyond any reasonable doubt, the user has returned from the
// camera
resolve();
} else {
// Probably not returned from camera, run another trial.
window.setTimeout(evtTimer, REQUESTED_DELAY_MS);
}
};
window.setTimeout(evtTimer, REQUESTED_DELAY_MS);
});
}
Я протестировал это на последних версиях iOS и Android, подняв собственную камеру, установив атрибуты для элемента <input />
.
<input type="file" accept="image/*" capture="camera" />
<input type="file" accept="video/*" capture="camcorder" />
На самом деле это работает намного лучше, чем я ожидал. Он запускает 10 испытаний, запрашивая таймер за 25 миллисекунд. Затем он измеряет, сколько времени на самом деле потребовалось для вызова, и если среднее из 10 испытаний составляет менее 50 миллисекунд, мы предполагаем, что мы должны быть на переднем плане, и камера исчезла. Если оно больше 50 миллисекунд, то мы все равно должны быть в фоновом режиме и продолжать ждать.
Некоторые дополнительные детали
Я использовал setTimeout()
вместо setInterval()
, поскольку последний может ставить в очередь несколько вызовов, которые выполняются сразу после друг друга. Это может значительно увеличить шум в наших данных, поэтому я остановился на setTimeout()
, хотя это немного сложнее.
Эти конкретные числа работали для меня хорошо, хотя я видел по крайней мере один раз случай, когда отклонение камеры было обнаружено преждевременно. Я полагаю, что это потому, что камера может медленно открываться, и устройство может выполнить 10 испытаний, прежде чем оно фактически станет фоновым. Обойти это можно, добавив больше испытаний или подождав около 25-50 миллисекунд перед запуском этой функции.
Desktop
К сожалению, это не работает для настольных браузеров. Теоретически возможен тот же трюк, поскольку они устанавливают приоритет текущей страницы над фоновыми страницами. Однако на многих настольных компьютерах достаточно ресурсов, чтобы страница работала на полной скорости, даже если она находится в фоновом режиме, поэтому на практике эта стратегия не работает.
Альтернативные решения
Одним из альтернативных решений, о котором мало кто упоминал, было то, что я издевался над FileList
.Мы начинаем с null
в <input />
, а затем, если пользователь открывает камеру и отменяет, он возвращается к null
, что не является изменением и никакое событие не сработает.Одним из решений было бы назначить фиктивный файл для <input />
в начале страницы, поэтому установка на null
будет изменением, которое вызовет соответствующее событие.
К сожалению, нет официального способа созданияFileList
, а элемент <input />
требует, в частности, FileList
и не будет принимать никаких других значений, кроме null
.Естественно, FileList
объекты не могут быть построены напрямую, это связано с какой-то старой проблемой безопасности, которая, по-видимому, уже не актуальна.Единственный способ получить доступ к одному элементу <input />
заключается в использовании хака, который копирует и вставляет данные, чтобы имитировать событие буфера обмена, которое может содержать объект FileList
(в основном вы притворяетесь перетаскиванием-a-file-on-your-website событие).Это возможно в Firefox, но не для iOS Safari, поэтому он не подходит для моего конкретного случая использования.
Браузеры, пожалуйста ...
Излишне говорить, что это явно смешно.Тот факт, что веб-страницы получают нулевое уведомление об изменении критического элемента пользовательского интерфейса, просто смешен.Это действительно ошибка в спецификации, так как она никогда не предназначалась для полноэкранного пользовательского интерфейса захвата мультимедиа, и событие «change» не вызывало технически к спецификации.
Однако , могут ли производители браузеров признать реальность этого?Это можно решить с помощью нового события «done», которое запускается, даже если никаких изменений не происходит, или вы можете просто вызвать «change» в любом случае.Да, это было бы против спецификации, но для меня тривиально дедуплировать событие изменения на стороне JavaScript, но принципиально невозможно изобрести мое собственное событие "done".Даже мое решение на самом деле просто эвристическое, если не дают никаких гарантий о состоянии браузера.
В его нынешнем виде этот API принципиально непригоден для мобильных устройств, и я думаю, что относительно простая замена браузера может сделать это бесконечнопроще для веб-разработчиков * сходит с мыла *.