Je vais donc jeter mon chapeau sur cette question depuis que j'ai trouvé une nouvelle solution. J'ai une application Web progressive qui permet aux utilisateurs de capturer des photos et des vidéos et de les télécharger. Nous utilisons WebRTC lorsque cela est possible, mais revenons aux sélecteurs de fichiers HTML5 pour les appareils avec moins de support * toux Safari toux *. Si vous travaillez spécifiquement sur une application Web mobile Android / iOS qui utilise l'appareil photo natif pour capturer directement des photos / vidéos, c'est la meilleure solution que j'ai rencontrée.
Le nœud de ce problème est que lorsque la page se charge, le file
est null
, mais lorsque l'utilisateur ouvre la boîte de dialogue et appuie sur "Annuler", le file
est toujours null
, donc il n'a pas "changé", donc aucun événement "changement" n'est déclenché. Pour les ordinateurs de bureau, ce n'est pas si mal, car la plupart des interfaces utilisateur de bureau ne dépendent pas du fait de savoir quand une annulation est invoquée, mais les interfaces utilisateur mobiles qui font apparaître l'appareil photo pour capturer une photo / vidéo dépendent beaucoup de savoir quand une annulation est activée.
J'ai initialement utilisé l' document.body.onfocus
événement pour détecter le retour de l'utilisateur depuis le sélecteur de fichiers, et cela a fonctionné pour la plupart des appareils, mais iOS 11.3 l'a cassé car cet événement n'est pas déclenché.
Concept
Ma solution à cela est * frémir * pour mesurer la synchronisation du processeur pour déterminer si la page est actuellement au premier plan ou en arrière-plan. Sur les appareils mobiles, le temps de traitement est donné à l'application actuellement au premier plan. Lorsqu'une caméra est visible, elle volera du temps CPU et dépriorisera le navigateur. Tout ce que nous avons à faire est de mesurer le temps de traitement accordé à notre page, lorsque la caméra démarre, notre temps disponible diminue considérablement. Lorsque la caméra est rejetée (annulée ou non), notre temps disponible augmente.
la mise en oeuvre
Nous pouvons mesurer la synchronisation du processeur en utilisant setTimeout()
pour appeler un rappel en X millisecondes, puis mesurer le temps qu'il a fallu pour l'invoquer. Le navigateur ne l'invoquera jamais exactement après X millisecondes, mais s'il est raisonnablement proche, nous devons être au premier plan. Si le navigateur est très loin (plus de 10 fois plus lent que demandé), nous devons être en arrière-plan. Une implémentation de base de ceci est comme ceci:
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);
});
}
J'ai testé cela sur une version récente d'iOS et d'Android, en faisant apparaître la caméra native en définissant les attributs sur l' <input />
élément.
<input type="file" accept="image/*" capture="camera" />
<input type="file" accept="video/*" capture="camcorder" />
Cela fonctionne en fait beaucoup mieux que ce à quoi je m'attendais. Il exécute 10 essais en demandant qu'un minuteur soit appelé en 25 millisecondes. Il mesure ensuite combien de temps il a réellement fallu pour invoquer, et si la moyenne de 10 essais est inférieure à 50 millisecondes, nous supposons que nous devons être au premier plan et que la caméra est partie. Si elle est supérieure à 50 millisecondes, alors nous devons toujours être en arrière-plan et continuer à attendre.
Quelques détails supplémentaires
J'ai utilisé setTimeout()
plutôt que setInterval()
parce que ce dernier peut mettre en file d'attente plusieurs invocations qui s'exécutent immédiatement les unes après les autres. Cela pourrait augmenter considérablement le bruit dans nos données, donc je suis resté avec setTimeout()
même si c'est un peu plus compliqué à faire.
Ces chiffres particuliers ont bien fonctionné pour moi, même si j'ai vu au moins une fois où le rejet de la caméra a été détecté prématurément. Je pense que c'est parce que la caméra peut être lente à s'ouvrir et que l'appareil peut exécuter 10 essais avant de devenir réellement en arrière-plan. L'ajout d'essais supplémentaires ou l'attente de 25 à 50 millisecondes avant de démarrer cette fonction peut être une solution de contournement pour cela.
Bureau
Malheureusement, cela ne fonctionne pas vraiment pour les navigateurs de bureau. En théorie, la même astuce est possible car ils donnent la priorité à la page actuelle par rapport aux pages en arrière-plan. Cependant, de nombreux bureaux disposent de suffisamment de ressources pour que la page continue à fonctionner à pleine vitesse, même en arrière-plan, cette stratégie ne fonctionne donc pas vraiment dans la pratique.
Solutions alternatives
Une solution alternative que peu de gens mentionnent que j'ai explorée était de se moquer d'un FileList
. Nous commençons par null
dans <input />
et puis si l'utilisateur ouvre la caméra et annule, il revient null
, ce qui n'est pas un changement et aucun événement ne se déclenchera. Une solution consisterait à attribuer un fichier factice <input />
au début de la page. Par conséquent, définir sur null
serait un changement qui déclencherait l'événement approprié.
Malheureusement, il n'y a aucun moyen officiel de créer un FileList
, et l' <input />
élément nécessite un FileList
en particulier et n'acceptera aucune autre valeur en plus null
. Naturellement, les FileList
objets ne peuvent pas être construits directement, à cause d'un vieux problème de sécurité qui n'est même plus pertinent apparemment. Le seul moyen d'en obtenir un en dehors d'un <input />
élément est d'utiliser un hack qui copie-colle des données pour FileList
simuler un événement de presse-papiers pouvant contenir un objet (vous simulez essentiellement un glisser-déposer d'un fichier sur -votre-événement de site Web). C'est possible dans Firefox, mais pas pour iOS Safari, donc ce n'était pas viable pour mon cas d'utilisation particulier.
Navigateurs, s'il vous plaît ...
Inutile de dire que c'est manifestement ridicule. Le fait que les pages Web ne reçoivent aucune notification indiquant qu'un élément critique de l'interface utilisateur a changé est tout simplement risible. C'est vraiment un bogue dans la spécification, car il n'a jamais été destiné à une interface utilisateur de capture multimédia en plein écran, et ne pas déclencher l'événement "changement" est techniquement conforme aux spécifications.
Cependant , les fournisseurs de navigateurs peuvent-ils reconnaître la réalité de cela? Cela peut être résolu soit avec un nouvel événement "done" qui est déclenché même lorsqu'aucun changement ne se produit, soit vous pouvez simplement déclencher "change" de toute façon. Ouais, ce serait contraire aux spécifications, mais il est trivial pour moi de déduire un événement de changement du côté JavaScript, mais fondamentalement impossible d'inventer mon propre événement "terminé". Même ma solution n'est vraiment qu'une heuristique, si elle n'offre aucune garantie sur l'état du navigateur.
Dans l'état actuel des choses, cette API est fondamentalement inutilisable pour les appareils mobiles, et je pense qu'un changement de navigateur relativement simple pourrait rendre cela infiniment plus facile pour les développeurs Web * sort de la boîte à savon *.
e.target.files