Então, vou jogar meu chapéu nessa questão, já que encontrei uma nova solução. Eu tenho um Progressive Web App que permite aos usuários capturar fotos e vídeos e carregá-los. Usamos WebRTC quando possível, mas recorremos a seletores de arquivo HTML5 para dispositivos com menos suporte * tosse Safari tosse *. Se você estiver trabalhando especificamente em um aplicativo da web móvel Android / iOS que usa a câmera nativa para capturar fotos / vídeos diretamente, esta é a melhor solução que encontrei.
O ponto crucial desse problema é que, quando a página carrega, o file
é null
, mas quando o usuário abre a caixa de diálogo e pressiona "Cancelar", o file
é null
, portanto, não "mudou", portanto, nenhum evento de "alteração" é acionado. Para desktops, isso não é tão ruim porque a maioria das IUs de desktops não dependem de saber quando um cancelamento é invocado, mas as IUs móveis que ativam a câmera para capturar uma foto / vídeo dependem muito de saber quando um cancelamento é pressionado.
Eu usei originalmente o document.body.onfocus
evento para detectar quando o usuário retornou do seletor de arquivos, e isso funcionou para a maioria dos dispositivos, mas o iOS 11.3 o quebrou porque o evento não é acionado.
Conceito
Minha solução para isso é * tremor * para medir o tempo da CPU para determinar se a página está atualmente em primeiro ou segundo plano. Em dispositivos móveis, o tempo de processamento é atribuído ao aplicativo atualmente em primeiro plano. Quando uma câmera está visível, ela rouba o tempo da CPU e diminui a prioridade do navegador. Tudo o que precisamos fazer é medir quanto tempo de processamento é dado à nossa página, quando a câmera for iniciada, nosso tempo disponível cairá drasticamente. Quando a câmera é dispensada (cancelada ou não), nosso tempo disponível aumenta.
Implementação
Podemos medir o tempo da CPU usando setTimeout()
para invocar um retorno de chamada em X milissegundos e, em seguida, medir quanto tempo levou para invocá-lo de fato. O navegador nunca o invocará exatamente após X milissegundos, mas se estiver razoavelmente perto, devemos estar em primeiro plano. Se o navegador estiver muito longe (mais de 10 vezes mais lento do que o solicitado), devemos estar em segundo plano. Uma implementação básica disso é assim:
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);
});
}
Eu testei isso em uma versão recente do iOS e Android, trazendo a câmera nativa definindo os atributos no <input />
elemento.
<input type="file" accept="image/*" capture="camera" />
<input type="file" accept="video/*" capture="camcorder" />
Na verdade, isso funciona muito melhor do que eu esperava. Ele executa 10 testes solicitando que um cronômetro seja chamado em 25 milissegundos. Em seguida, mede quanto tempo realmente levou para invocar e, se a média de 10 tentativas for inferior a 50 milissegundos, presumimos que devemos estar em primeiro plano e a câmera sumiu. Se for maior que 50 milissegundos, ainda devemos estar em segundo plano e continuar aguardando.
Alguns detalhes adicionais
Usei em setTimeout()
vez de setInterval()
porque o último pode enfileirar várias invocações que são executadas imediatamente uma após a outra. Isso poderia aumentar drasticamente o ruído em nossos dados, então continuei setTimeout()
, embora seja um pouco mais complicado de fazer.
Esses números específicos funcionaram bem para mim, embora eu tenha visto pelo menos uma vez em que o descarte da câmera foi detectado prematuramente. Acredito que isso seja porque a câmera pode demorar para abrir e o dispositivo pode executar 10 tentativas antes de realmente entrar em segundo plano. Adicionar mais tentativas ou esperar cerca de 25-50 milissegundos antes de iniciar esta função pode ser uma solução alternativa para isso.
Área de Trabalho
Infelizmente, isso realmente não funciona para navegadores de desktop. Em teoria, o mesmo truque é possível, pois eles priorizam a página atual sobre as páginas em segundo plano. No entanto, muitos desktops têm recursos suficientes para manter a página em execução em velocidade total, mesmo quando em segundo plano, então essa estratégia não funciona na prática.
Soluções alternativas
Uma solução alternativa que poucas pessoas mencionam e que explorei foi zombar de a FileList
. Começamos com null
no <input />
e, em seguida, se o usuário abrir a câmera e cancelar, eles voltam null
, o que não é uma alteração e nenhum evento será acionado. Uma solução seria atribuir um arquivo fictício ao <input />
início da página, portanto, definir como null
seria uma alteração que acionaria o evento apropriado.
Infelizmente, não existe uma maneira oficial de criar um FileList
, e o <input />
elemento requer um FileList
em particular e não aceitará nenhum outro valor além disso null
. Naturalmente, os FileList
objetos não podem ser construídos diretamente, devido a algum velho problema de segurança que aparentemente não é mais relevante. A única maneira de obter alguém de fora de um <input />
elemento é utilizar um hack que copia e cola dados para simular um evento da área de transferência que pode conter um FileList
objeto (você está basicamente fingindo arrastar e soltar um arquivo evento do seu site). Isso é possível no Firefox, mas não para iOS Safari, portanto, não era viável para meu caso de uso específico.
Navegadores, por favor ...
Nem é preciso dizer que isso é ridículo. O fato de que as páginas da web não recebem nenhuma notificação de que um elemento crítico da IU foi alterado é simplesmente ridículo. Este é realmente um bug na especificação, já que nunca foi planejado para uma IU de captura de mídia em tela inteira, e não acionar o evento "alterar" é tecnicamente uma especificação.
No entanto , os fornecedores de navegadores podem reconhecer a realidade disso? Isso pode ser resolvido com um novo evento "concluído", que é acionado mesmo quando nenhuma alteração ocorre, ou você pode simplesmente acionar "alteração" de qualquer maneira. Sim, isso seria contra a especificação, mas é trivial para mim desduplicar um evento de alteração no lado do JavaScript, mas fundamentalmente impossível inventar meu próprio evento "concluído". Mesmo minha solução é realmente apenas heurística, se não oferecer garantias sobre o estado do navegador.
Do jeito que está, essa API é fundamentalmente inutilizável para dispositivos móveis e acho que uma mudança relativamente simples no navegador poderia tornar isso infinitamente mais fácil para os desenvolvedores da web * sair da caixa de sabão *.
e.target.files