De nombreuses caméras peuvent diffuser des vidéos en utilisant Motion JPEG. Il s’agit d’un flux d’images JPEG pouvant être affichées en séquence pour former une vidéo. De nombreuses caméras IP utilisent une requête GET HTTP vers une adresse IP. La caméra que j’utilise dans cet exemple est une caméra RICOH THETA 360° qui nécessite une commande POST. Plus d’informations sur la façon dont cette caméra particulière gère MotionJPEG sont disponibles dans la communauté. Que vous utilisiez une requête HTTP GET ou POST pour démarrer le flux motionJPEG, le traitement est le même.
Résolution et fréquence d’images Motion JPEG
Motion JPEG envoie un flux d’images JPEG à une résolution et une fréquence d’images spécifiées. La modification de la résolution et de la fréquence d’images affecte à la fois les exigences de transmission de données et la latence.
L’appareil photo que j’utilise peut afficher Motion JPEG aux résolutions et fréquences d’images suivantes.
{“width”: 1920, “height”: 960, “framerate”: 8}
{“width”: 1024, “height”: 512, “framerate”: 30}
{“width”: 1024, “height”: 512, “framerate”: 8}
{“width”: 640, “height”: 320, “framerate”: 30}
{“width”: 640, “height”: 320, “framerate”: 8}
Motion JPEG est facile à expérimenter car les images peuvent être enregistrées sur le disque en tant qu’images JPEG standard.
Avant même d’afficher les images sous forme de vidéo en direct dans votre application mobile, vous pouvez expérimenter les exigences de dimensionnement, de mise en page et de transfert de données sous forme d’images JPEG. Les cadres de cet exemple sont au format équirectangulaire car j’utilise une caméra 360°. L’algorithme est le même pour les caméras standard ou les caméras IP grand angle 180°.
Acquisition d’un flux d’octets
Les trames JPEG sont codées sous forme de flux d’octets.
Cet extrait de code dans Dart lancera un flux d’octets à partir de la ligne de commande. Vous devez adapter la requête HTTP à l’API spécifique de votre caméra. De nombreuses caméras IP offrent une requête GET simple au point de terminaison de l’API. Reportez-vous à la documentation API de votre caméra spécifique. Bien que le démarrage du flux puisse être différent selon la caméra, le format du flux Motion JPEG est généralement le même pour tous les modèles de caméra.
Le point important à comprendre est que la réponse est un flux et que vous devez écouter le flux.
import 'dart:convert';
import 'dart:io';
void main() async {
Uri apiUrl = Uri.parse('http://192.168.1.1/osc/commands/execute');
var client = HttpClient();
Map<String, String> body = {'name': 'camera.getLivePreview'};
var request = await client.postUrl(apiUrl)
..headers.contentType = ContentType("application", "json", charset: "utf-8")
..write(jsonEncode(body));
var response = await request.close();
response.listen((List<int> data) {
print(data);
});
}
La sortie durera éternellement. Appuyez sur CTRL-C pour arrêter le programme.
...
, 49, 40, 162, 138, 162, 69, 237, 73, 75, 73, 64, 5, 20, 81, 64, 5, 20, 81, 64, 5, 20, 81, 64, 5, 20, 81, 64, 5, 20, 81, 64, 5, 20, 81, 64, 5, 45, 37, 45, 0, 37, 20, 81, 64, 5, 20, 81, 64, 5,
20, 81, 64, 5, 20, 81, 64, 5, 20, 81, 64, 5, 20, 81, 64, 5, 20^CTerminate batch job (Y/N)? y
Toutes nos félicitations. Vous avez maintenant un bon flux d’octets. Parcourons le flux et recherchons les cadres.
response.listen((List<int> data) {
for (var i = 0; i < data.length - 1; i++) {
}
Trouver où chaque image commence et se termine
Pour transformer le flux en images JPEG, vous devez rechercher les octets de début et de fin.
Chaque image commence par 0xff 0xd8
et se termine par 0xff 0xd9
.
Voici un extrait de code qui montre comment parcourir chaque donnée du flux et rechercher une correspondance avec les octets de début et de fin.
videoStream = response.stream.listen((List<int> data) async {
buf.addAll(data);
for (var i = 0; i < data.length - 1; i++) {
if (data[i] == 0xFF && data[i + 1] == 0xd8) {
startIndex = i;
print('found frame start');
}
if (data[i] == 0xff && data[i + 1] == 0xd9) {
endIndex = buf.length;
print('found frame end');
}
}
Une fois que vous avez trouvé le début et la fin de chaque cadre, vous pouvez enregistrer le cadre dans un fichier pour inspection. L’exemple ci-dessous enregistre une seule image dans un fichier.
///extract single frame from motionjpeg stream from
///RICOH THETA Z1 livePreview
import 'dart:async';
import 'dart:convert';
import 'dart:io';
main(List<String> args) async {
File fileHandle = File('test_frame.jpg');
Uri url = Uri.parse('http://192.168.1.1/osc/commands/execute');
var client = HttpClient();
Map<String, String> bodyMap = {"name": "camera.getLivePreview"};
var request = await client.postUrl(url)
..headers.contentType = ContentType("application", "json", charset: "utf-8")
..write(jsonEncode(bodyMap));
var response = await request.close();
StreamSubscription? videoStream;
var startIndex = -1;
var endIndex = -1;
List<int> buf = [];
videoStream = response.listen(
(List<int> data) {
buf.addAll(data);
for (var i = 0; i < data.length - 1; i++) {
if (data[i] == 0xFF && data[i + 1] == 0xD8) {
startIndex = i;
}
if (data[i] == 0xff && data[i + 1] == 0xd9) {
endIndex = buf.length;
}
}
if (startIndex != -1 && endIndex != -1) {
print('saving frame');
fileHandle.writeAsBytes(buf.sublist(startIndex, endIndex));
print('finished saving frames');
if (videoStream != null) {
videoStream.cancel();
client.close();
}
}
},
);
}
L’image peut maintenant être ouverte sur votre poste de développement. Dans l’exemple ci-dessous, j’ouvre le fichier JPEG sous Windows 10
Vous pouvez également inspecter les cadres pour des paramètres tels que les dimensions en cliquant avec le bouton droit sur le fichier dans Windows et en regardant les propriétés du fichier.
Affichage de la vidéo
Pour afficher l’image sur l’écran Flutter, vous utilisez Image.memory()
. Pour afficher toutes les images du flux, vous devez utiliser un StreamBuilder()
qui reconstruit l’image affichée chaque fois qu’une nouvelle image entre dans le flux.
Il s’agit d’un court extrait qui montre la partie de l’application qui accepte un flux d’images Motion JPEG et les affiche dans la fenêtre Flutter Image.
class _LivePreviewState extends State<LivePreview> {
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: widget.controller.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
var imageData = Uint8List.fromList(snapshot.data);
return Image.memory(
imageData,
gaplessPlayback: true,
);
} else {
return Container();
}
},
Le paramètre gaplessPlayback
est intégré à Flutter. Cela élimine le scintillement sur les flux vidéo si une image est corrompue. En activant gaplessPlayback
dans votre Flutter Image, l’application conservera une bonne image jusqu’à ce qu’une autre bonne image soit reçue. À 30 images par seconde, vous ne remarquerez probablement pas la ou les images manquées.
Utiliser Uint8List
, ce qui est nécessaire pour Image.memory()
, vous devez importer dart:typed_data
.
Conclusion
L’utilisation de Motion JPEG avec des appareils photo est très amusante pour les développeurs, car les images JPEG sont faciles à obtenir et à gérer. Il existe de nombreuses bibliothèques qui géreront Motion JPEG pour vous. Cependant, c’est une excellente expérience d’apprentissage pour ouvrir votre propre session HTTP avec une simple requête GET ou POST et inspecter les données vous-même. Comme vous recevez un flux de données, c’est une excellente occasion d’expérimenter avec des flux et comment itérer à travers les données. Il est également gratifiant d’enregistrer les images sur le disque ou de les afficher sur votre application mobile. Comme Motion JPEG est accessible avec HTTP standard, il est facile d’obtenir un flux en temps réel via Wi-Fi ou un réseau plus large.