Así que me meteré en esta pregunta ya que se me ocurrió una solución novedosa. Tengo una aplicación web progresiva que permite a los usuarios capturar fotos y videos y subirlos. Usamos WebRTC cuando es posible, pero recurrimos a los selectores de archivos HTML5 para dispositivos con menos soporte * tos de Safari *. Si está trabajando específicamente en una aplicación web móvil Android / iOS que usa la cámara nativa para capturar fotos / videos directamente, entonces esta es la mejor solución que he encontrado.
El quid de este problema es que cuando se carga la página, el file
es null
, pero cuando el usuario abre el cuadro de diálogo y presiona "Cancelar", el file
es todavía null
, por lo que no "cambió", por lo que no se activa ningún evento de "cambio". Para las computadoras de escritorio, esto no es tan malo porque la mayoría de las interfaces de usuario de escritorio no dependen de saber cuándo se invoca una cancelación, pero las UI móviles que muestran la cámara para capturar una foto / video dependen mucho de saber cuándo se presiona una cancelación.
Originalmente usé el document.body.onfocus
evento para detectar cuándo el usuario regresó del selector de archivos, y esto funcionó para la mayoría de los dispositivos, pero iOS 11.3 lo rompió ya que ese evento no se activa.
Concepto
Mi solución a esto es * estremecimiento * para medir el tiempo de la CPU para determinar si la página está actualmente en primer plano o en segundo plano. En los dispositivos móviles, el tiempo de procesamiento se asigna a la aplicación que se encuentra actualmente en primer plano. Cuando una cámara es visible, robará tiempo de CPU y despriorizará al navegador. Todo lo que tenemos que hacer es medir cuánto tiempo de procesamiento se le da a nuestra página, cuando se inicia la cámara, nuestro tiempo disponible se reducirá drásticamente. Cuando se desconecta la cámara (ya sea cancelada o no), nuestro tiempo disponible aumenta de nuevo.
Implementación
Podemos medir el tiempo de la CPU usando setTimeout()
para invocar una devolución de llamada en X milisegundos y luego medir cuánto tiempo tomó realmente invocarla. El navegador nunca lo invocará exactamente después de X milisegundos, pero si está razonablemente cerca, entonces debemos estar en primer plano. Si el navegador está muy lejos (más de 10 veces más lento de lo solicitado) entonces debemos estar en segundo plano. Una implementación básica de esto es así:
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);
});
}
Probé esto en una versión reciente de iOS y Android, abriendo la cámara nativa configurando los atributos en el <input />
elemento.
<input type="file" accept="image/*" capture="camera" />
<input type="file" accept="video/*" capture="camcorder" />
En realidad, esto funciona mucho mejor de lo que esperaba. Ejecuta 10 pruebas solicitando que se invoque un temporizador en 25 milisegundos. Luego mide cuánto tiempo realmente tomó invocar, y si el promedio de 10 intentos es menos de 50 milisegundos, asumimos que debemos estar en primer plano y la cámara no está. Si es superior a 50 milisegundos, entonces todavía debemos estar en segundo plano y debemos seguir esperando.
Algunos detalles adicionales
Usé en setTimeout()
lugar de setInterval()
porque este último puede poner en cola múltiples invocaciones que se ejecutan inmediatamente una después de la otra. Esto podría aumentar drásticamente el ruido en nuestros datos, así que me quedé setTimeout()
a pesar de que es un poco más complicado hacerlo.
Estos números en particular funcionaron bien para mí, aunque he visto al menos una instancia en la que el disparo de la cámara se detectó prematuramente. Creo que esto se debe a que la cámara puede tardar en abrirse y el dispositivo puede ejecutar 10 pruebas antes de que se vuelva a poner en segundo plano. Agregar más pruebas o esperar entre 25 y 50 milisegundos antes de iniciar esta función puede ser una solución alternativa.
Escritorio
Desafortunadamente, esto realmente no funciona para los navegadores de escritorio. En teoría, el mismo truco es posible, ya que priorizan la página actual sobre las páginas en segundo plano. Sin embargo, muchos escritorios tienen suficientes recursos para mantener la página funcionando a toda velocidad incluso cuando están en segundo plano, por lo que esta estrategia no funciona en la práctica.
Soluciones alternativas
Una solución alternativa que no mucha gente menciona que exploré fue burlarme de un FileList
. Comenzamos con null
en <input />
y luego, si el usuario abre la cámara y cancela, regresa null
, lo cual no es un cambio y no se activará ningún evento. Una solución sería asignar un archivo ficticio al <input />
inicio de la página, por lo tanto, establecer ennull
sería un cambio que desencadenaría el evento apropiado.
Desafortunadamente, no hay forma oficial de crear un FileList
, y el <input />
elemento requiere un FileList
en particular y no aceptará ningún otro valor además null
. Naturalmente, los FileList
objetos no se pueden construir directamente, debido a algún problema de seguridad antiguo que aparentemente ya no es relevante. La única forma de conseguir uno fuera de un <input />
elemento es utilizar un truco que copia y pega datos para simular un evento de portapapeles que puede contener un FileList
objeto (básicamente estás fingiendo arrastrar y soltar un archivo en evento de su sitio web). Esto es posible en Firefox, pero no para iOS Safari, por lo que no era viable para mi caso de uso particular.
Navegadores, por favor ...
No hace falta decir que esto es evidentemente ridículo. El hecho de que a las páginas web no se les notifique que un elemento crítico de la interfaz de usuario ha cambiado es simplemente ridículo. Esto es realmente un error en la especificación, ya que nunca fue diseñado para una interfaz de usuario de captura de medios a pantalla completa, y no activar el evento de "cambio" es técnicamente conforme a las especificaciones.
Sin embargo , ¿pueden los proveedores de navegadores reconocer la realidad de esto? Esto podría resolverse con un nuevo evento "hecho" que se activa incluso cuando no se produce ningún cambio, o simplemente puede activar "cambio" de todos modos. Sí, eso iría en contra de las especificaciones, pero es trivial para mí deducir un evento de cambio en el lado de JavaScript, aunque fundamentalmente imposible inventar mi propio evento "hecho". Incluso mi solución es realmente solo heurística, si no ofrezco garantías sobre el estado del navegador.
Tal como está, esta API es fundamentalmente inutilizable para dispositivos móviles, y creo que un cambio de navegador relativamente simple podría hacer esto infinitamente más fácil para los desarrolladores web * pasos fuera de la caja de jabón *.
e.target.files