Comment streamer le flux d'une webcam sur une page web en python

a marqué ce sujet comme résolu.

Bonjour,

Pour un projet, nous devons faire en sorte de streamer le flux video d’une caméra sur une page web. Pour cela nous utilisons opencv pour capturer la vidéo puis nous l’envoyons via gstreamer sur un port udp. On récupère ensuite ce flux gstreamer avec Opencv et on l’affiche dans un serveur web fait via flask.

Les problèmes sont les suivants :

  • Nous avons une perte importante de 'key-frame" lors de l’envoie du flux en h264 ce qui fait que sur le serveur les images s’affichent très bruités
  • Nous avons une grande latence

Voici un un exemple minimal pour repoduire notre application :

Programme python d’envoie du flux video

import cv2


cap = cv2.VideoCapture(0)  # open the camera

if cap.isOpened():
    # get vcap property
    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))  # float
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))  # float
else:
    width = 1280
    height = 720

fourcc = cv2.VideoWriter_fourcc(*'H264')
out = cv2.VideoWriter('appsrc ! videoconvert ! x264enc tune=zerolatency noise-reduction=0 speed-preset=ultrafast ! rtph264pay config-interval=1 pt=96 ! udpsink host=127.0.0.1 port=5000', fourcc, cv2.CAP_PROP_FPS, (1280, 720), True) #ouput GStreamer pipeline

if not out.isOpened():
    print('VideoWriter not opened')
    exit(0)

while cap.isOpened():
    ret, frame = cap.read()

    if ret:

        # Write to pipeline
        frame = cv2.resize(frame, (1280, 720))
        out.write(frame)

        cv2.imshow("test", frame)

        if cv2.waitKey(1)&0xFF == ord('q'):
            break

cap.release()
out.release()

Serveur de réception

import cv2
from flask import Flask, render_template, Response
import os

ip = "127.0.0.1"
port = 5001

cap = cv2.VideoCapture(
            'udpsrc port=5000 caps = "application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264, payload=(int)96" ! rtph264depay ! decodebin ! videoconvert ! appsink',
            cv2.CAP_GSTREAMER)

if not cap.isOpened():
    print('VideoCapture not opened')
    raise ValueError("Cam with Gstreamer pipeline not opened")


def generate():

    while True:
        ret, frame = cap.read()

        if not ret:
            print('empty frame')
            break

        frame = cv2.flip(frame, 1)

        (flag, frame) = cv2.imencode(".png", frame)

        yield b'--frame\r\n' b'Content-Type: image/png\r\n\r\n' + bytearray(frame) + b'\r\n'


def create_app(ip, port, test_config=None):
    # create and configure the app

    app = Flask(__name__, instance_relative_config=True)  # creation of the app
    app.config.from_mapping(
        SECRET_KEY='dev',
        DATABASDE=os.path.join(app.instance_path, 'flaskr.sqlite')
    )

    if test_config is None:
        app.config.from_pyfile('config.py', silent=True)
    else:
        app.config.from_mapping(test_config)

    try:
        os.makedirs(app.instance_path)  # create instance directory because flask doesn't create it on its own
    except OSError:
        pass

    @app.route('/')
    def init_page():
        return render_template('home.html', ip=ip, port=port)

    @app.route('/video_feed')
    def video_feed():
        return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame")

    return app


if __name__ == "__main__":
    app = create_app(ip, port)
    app.run(host=ip, port=port, debug=True, threaded=True, use_reloader=False)

Pour que le serveur fonctionne, il faut, dans le même dossier que le fichier serveur, créer un dossier nommé "templates" et y mettre la page html suivante :

<html>
  <head>
    <title>Front Cam</title>
  </head>
  <body>
    <h1>This is Front cam</h1> <br />
    <img src="{{ url_for('video_feed') }}">
  </body>
</html>

Auriez-vous une idée de pourquoi nous avons ces problèmes avec notre solutions ? Avez-vous d’autres propositions de solutions pour envoyer une vidéo à travers le réseau et l’afficher sur une page web ?

Merci d’avance, Bonne journée!

Bonjour,

Merci pour votre réponse.

J’ai déjà essayé de récupérer le pipe gstreamer via videojs sur une page web. Néanmoins, comme je n’y connais pas grand chose en langages web, je suis passé par la solution qui était plus simple avec mes connaissances : recevoir via python et envoyer les images une par une à la page html.

Auriez-vous un exemple ou une doc concernant la balise vidéo en html pour récupérer mon pipe gstreamer ?

Merci

Oui il est possible de le faire via gstreamer. Je viens de regarder la doc et un exemple basique d’envoie serait :

gst-launch-1.0 -v videotestsrc ! ffenc_flv ! flvmux ! rtmpsink location='rtmp://localhost/path/to/stream live=1'

Je peux aussi générer un flux rstp ou rtp. Le problème étant la réception sur la page web sachant que j’ai oublié de préciser que je suis forcé d’utiliser le codec h264

Si tu peux faire un flux HLS, c’est peut être le plus simple pour de la lecture Web (c’est ce qu’utilisent la plupart des sites de live et de streaming de nos jours).

L’avantage c’est que tu dois juste gérer plein de petits fichiers statiques et c’est ton player JavaScript qui fera le boulot d’aller récupérer les bons au bon moment pour les lire de façon continue.

Il y a quelques années j’avais fait un projet similaire dont voici la chaîne :

  • capture de la Web cam avec ffmpeg et génération à la volée du flux HLS consistant en un fichier d’index, le m3u8 et les petits fichiers vidéos TS (les miens étaient en h264) qui sortent en continu. C’est bien ffmpeg qui fait tout ça.
  • serveur HTTP simple (nginx) qui pointe sur le dossier où sont générés les fichiers vidéos en continu.
  • page HTML intégrant un player JS sachant gérer le HLS : de mémoire, c’était un player libre maintenu par DailyMotion.

Je n’ai pas eu besoin de code applicatif Python ou autre. En revanche, j’avais fait un petit wrapper en Bash autour de ffmpeg pour me faciliter la vie.

Peut-être que, comme ffmpeg, gstreamer aussi est capable de faire ce travail de captation+encodage+génération HLS.

P.-S. : j’ai retrouvé le player JS dont voici le lien : https://github.com/dailymotion/hls.js/tree/master

+3 -0

Tu es sur de toi ? Le HLS ça me semble adapté pour des vidéos dont tu as déjà connaissances a priori (et que donc tu peux découper en segments offline et tu as juste à stocker tout ce petit monde) mais ça marche pas pour du live.

Un live tu feras plutôt un flux original 'x' (disons 1080p) qui sera transcodé au fil de l’eau. X sera ton flux original que tu renvoies telle qu’elle si les utilisateurs le supporte et si le client voit qu’il buffer trop il ira plutôt se mettre sur une route dégradé (y en 720p ou z en 480p par exemple).

Par contre autant envoyer un flux ça demande juste de la bande passante, envoyer un flux + des flux dégradé ça va demander de la bande passante et un bon processeur pour transcoder au fil de l’eau.

+1 -0

Je suis plutôt sûr, oui. Je l’ai mis en œuvre moi-même pour un flux live en continu depuis ma webcam. Mais même pour des vidéos non-live, la technique marche très bien aussi. Elle apporte cependant son lot inconvénients, notamment le fait de devoir gérer plusieurs fichiers au lieu d’un seul.

Par contre autant envoyer un flux ça demande juste de la bande passante, envoyer un flux + des flux dégradé ça va demander de la bande passante et un bon processeur pour transcoder au fil de l’eau.

Attention, on ne télécharge pas tous les flux ! Le client télécharge uniquement le flux qui l’intéresse. s’il veut la version 1080p, il télécharge uniquement les segments 1080p, même si les autres peuvent être indiqués dans le manifeste M3U8 au cas où il voudrait changer par la suite.
De plus, rien ne t’oblige à faire plusieurs flux pour gérer des qualités différentes. Tu peux en faire qu’un seul (ce que j’ai fait).

Tu soulèves un point que je n’ai pas abordé : ça demande effectivement un peu de puissance pour transcoder à la volée en temps et en heure. Mon i5 de l’époque faisait le boulot avec les presets de transcodage que je lui avais indiqués. J’ai utilisé un profil H264 qui a la particularité d’être rapide à encoder, quitte à rogner sur la qualité de sortie (preset fast je crois).

Tu es sur de toi ? Le HLS ça me semble adapté pour des vidéos dont tu as déjà connaissances a priori (et que donc tu peux découper en segments offline et tu as juste à stocker tout ce petit monde) mais ça marche pas pour du live.

C’est pourtant ce qui est utilisé sur YouTube* et Twitch. Si tu ouvres une console Web et que tu inspectes l’activité réseau sur un live, tu verras qu’il s’agit bien de ça : un fichier M3U8 récupéré périodiquement et tous les segments nécessaires pour jouer la vidéo en continu.

Récupération périodique du manifeste M3U8 (rouge) et des segments associés (vert)
Récupération périodique du manifeste M3U8 (rouge) et des segments associés (vert)

* : YouTube a l’air d’utiliser une version un peu plus custom/obfuscée, mais le principe est le même.

+1 -0

Attention, on ne télécharge pas tous les flux ! Le client télécharge uniquement le flux qui l’intéresse. s’il veut la version 1080p, il télécharge uniquement les segments 1080p, même si les autres peuvent être indiqués dans le manifeste M3U8 au cas où il voudrait changer par la suite. De plus, rien ne t’oblige à faire plusieurs flux pour gérer des qualités différentes. Tu peux en faire qu’un seul (ce que j’ai fait).

Oui oui je me suis peut etre mal exprimé, c’était dans le sens qu’il faudra "comme précédent + de la puissance"

Du coup my bad pour Twitch et compagnie, j’étais persuadé que le HLS ne pouvait pas s’appliquer à du temps réel (car pour découper en segment en temps réel il faudra forcément ajouter un lag important non ?)

+1 -0

Du coup my bad pour Twitch et compagnie, j’étais persuadé que le HLS ne pouvait pas s’appliquer à du temps réel (car pour découper en segment en temps réel il faudra forcément ajouter un lag important non ?)

Le découpage en tant que tel ne coûte rien en puissance de calcul. C’est vraiment le transcodage flux webcam -> H264 * qui est coûteux et il faut faire en sorte que ce soit fait dans le temps imparti, en effet.

Je précise quand même que cette opération de transcodage est nécessaire uniquement parce que le flux de sortie de ma webcam n’était pas adéquat. S’il eut été, ffmpeg aurait simplement fait un travail de découpage et d’agencement d’un « flux » HLS (i.e. segments TS + manifeste M3U8 mis à jour constamment). Cette opération est quasiment gratuite en calcul (ffmpeg : -c:v copy indique de juste copier le flux vidéo tel qu’il est déjà).

À la limite, le coût induit est celui du volume de données à transporter car pour chaque segment vidéo, il faut se retaper les headers HTTP et les headers des fichiers TS. De même, il faut à intervalle régulier re-télécharger le manifeste M3U8 qui change continuellement (dans le cas d’un live). Cependant, trois points pour nuancer :

  • Le premier problème apparaît aussi de la même façon avec un seul gros fichier vidéo sur lequel on ferait des requêtes HTTP de range, technique de streaming de vidéo avant qu’on utilise HLS
  • Depuis la généralisation de HTTP/2 et son mécanisme de multiplexage et de compression des headers HTTP, je ne suis pas sûr qu’il s’agisse là d’un problème sérieux : le fait de faire plein d’appels HTTP a été optimisé de façon satisfaisante.
  • Il s’agit pour le serveur de servir des fichiers statiques. C’est donc chose très aisée et optimisée, même sous un nombre de requêtes important. La charge du serveur est donc simplement la charge d’un serveur de fichier simple. L’autre avantage c’est que les petits segments sont cachables et distribuables sur des CDN.

* : je précise H264 car dans mon exemple c’est ce que j’ai utilisé. Mais ça aurait pu être autre chose comme du VP9 par exemple. Dans tous les cas, cette opération de transcodage est très coûteuse.

+2 -0

Ma remarque sur le lag était plutôt du genre "pour faire un segment il faut un bout de vidéo" du coup forcément faut attendre que ce morceau de vidéo soit tourné/dispo pour pouvoir le transcoder et mettre à disposition non ?

Eskimon

Ah, je comprends ! Oui, tu as un « lag » pour le premier segment seulement, qui correspond au minimum à sa longueur temporelle. Tu peux régler ça dans diverses options du logiciel que tu utilises pour générer tes segments (en général entre 2–5 secondes ça se fait). Tu peux transcoder à la volée sans étape intermédiaire. Finalement c’est un détail d’implémentation du logiciel que tu vas utiliser, savoir s’il va bufferiser et comment. Les segments suivants le premiers n’ont pas de « lag » car le tout est pipelinisé.

Mais la chose important c’est que dans tous les cas, tu dois bien entendu avoir assez de puissance pour que le contenu de n secondes puisse être transcodé et mis à disposition sous forme de segment en moins de n secondes. Si tu ne peux pas être assez rapide, le système ne fonctionne plus.

De même, le client doit pouvoir télécharger un contenu de n secondes en moins de n secondes s’il veut pouvoir lire les segments sans discontinuité. Les players JS sont en général assez intelligent et prévoient de la marge : quand tu lis le segment S, il y a déjà les segments S+1 et S+2 prêts à être joués au cas où la connexion faiblirait quelques instants.

Concernant la vitesse de transco, le nerf de la guerre : ffmpeg t’indique sa vitesse de transcodage. Si c’est 0.99 ou 1, c’est ok, les segments sont générés à temps pour être servis à temps. Par contre, si c’est moins, ça vaut dire que ta génération ne va pas aussi vite que le flux d’entrée ce qui mènera forcément à une coupure temporaire dans un futur plus ou moins proche selon la vitesse (même si c’est assez proche de 1, style 0.90).

+3 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte