angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Galerie vidéo simple avec Flask, Jinja, Bootstrap et JQuery

Afficher les vidéos par date et heure en un minimum de clics avec Flask, Jinja, Bootstrap et JQuery.

28 octobre 2024
Dans Flask
post main image
https://www.pexels.com/nl-nl/@mo3ath-photos-110226063

Nous disposons d'un certain nombre de caméras qui génèrent de courtes séquences lorsque quelque chose se déplace devant la caméra. Tous les clips, les vidéos, aboutissent sur un seul système. Nous utilisons des programmes standard pour visionner les vidéos.

Mais nous voulons maintenant partager ces vidéos avec d'autres personnes sur notre réseau local, et nous ne voulons pas copier les fichiers vidéo. La solution la plus évidente consiste à choisir et à installer un serveur de galerie vidéo. Mais, hé, c'est un blog sur Python, et c'est un petit projet, donc nous le créons nous-mêmes. Nous n'implémentons pas HTTPS, ou login, ce qui signifie que n'importe qui sur le réseau local peut voir les vidéos. Si vous le souhaitez, vous pouvez restreindre l'accès par IP address.

Comme toujours, je fais cela sur Ubuntu 22.04, et les autres machines du réseau local tournent également sous Ubuntu.

Logiciel de galerie vidéo disponible ou projet personnalisé

Pouvons-nous choisir quelque chose comme le logiciel de galerie vidéo open source ? Le problème est la fonctionnalité requise dans ce cas.
Nous voulons minimiser le nombre de clics pour visionner une vidéo, et pour visionner la vidéo suivante, etc. Cela nécessite une liste de vidéos toujours visible avec la durée des vidéos, et la liste peut être longue. La plupart des galeries d'images et de vidéos affichent une page de vignettes et un clic signifie qu'une nouvelle page s'ouvre et que nous devons revenir en arrière pour sélectionner une autre vidéo. Ce n'est pas ce que nous voulons !

En résumé, notre écran devrait ressembler à ceci :

  +-------------------+-------------------------------+
  |   <<              |                               |
  | > 2024-10-27      |                               |
  | v 2024-10-26      |                               |
  |   CAM01  18:54:48 |                               |
  |   CAM01  18:53:17 |            VIDEO              |
  |   ...             |                               |
  |   ...             |                               |
  |   ...             |                               |
  | > 2024-10-25      |                               |
  | > 2024-10-24      |                               |
  |   ...             |                               |
  |                   |                               |
  |                   |                               |
  |                   |                               |
  +-------------------+-------------------------------+

Cliquez sur une durée et la vidéo s'affiche et s'exécute. Oh, et il n'a pas besoin d'être réactif, nous le regardons toujours sur un moniteur.

Le projet

C'est une tâche parfaite pour Flask et Jinja. J'ai déjà Nginx sur ma machine de développement. Les vidéos sont au format 'MP4' et sont stockées dans des dossiers dont les noms sont des dates. Flask ne sert qu'à sélectionner une date et des vidéos. Le fait de cliquer sur un lien vidéo n'ouvre pas une nouvelle page mais lit la vidéo sélectionnée dans la fenêtre vidéo de la page.

Voici la structure du répertoire du projet :

├── app
│   ├── main.py
│   ├── static
│   │   └── videos
│   │       ├── 2024-10-23
│   │       │   └── ...
│   │       ├── 2024-10-24
│   │       │   └── ...
│   │       ├── 2024-10-25
│   │       │   ├── CAM01_101-01-20241025031350.mp4
│   │       │   ├── CAM01_101-01-20241025161248.mp4
│   │       │   └── ...
│   │       ├── 2024-10-26
│   │       │   ├── CAM01_101-01-20241026185317.mp4
│   │       │   ├── CAM01_101-02-20241026185448.mp4
│   │       │   └── ...
│   │       ├── CAM01_101-04-20241025231354.mp4
│   │       ├── CAM02_102-05-20241025231454.mp4
│   │       └── ...
│   └── templates
│       └── index.html
├── run.py

Créer un virtual environment, un répertoire de projet, et installer Flask.

Nginx

Nous utilisons Nginx pour servir notre application :

  • L'application Flask , utilisant un serveur reverse proxy .
  • Les vidéos, à partir d'un répertoire
  • Le contenu statique, à partir d'un répertoire

Comme toutes les données ne proviennent pas de notre application Flask , nous attribuons un nom d'hôte (serveur) à notre application :

motion-videos

Nous éditons :

/etc/hosts

et ajoutons la ligne :

127.0.0.1 motion-videos

Sur les autres machines de notre réseau local, nous ajoutons également cette ligne, mais maintenant avec le IP address de notre machine de développement, celle qui contient les vidéos et qui exécute l'application.

Ensuite, pour appeler notre application dans notre navigateur, nous tapons dans la barre d'adresse :

http://motion-videos

Nginx sait maintenant de quel serveur nous parlons, et peut également servir les vidéos et le contenu statique.

Voici le fichier du serveur Nginx , notre application est sur le port 6395, n'hésitez pas à le changer.

# /etc/nginx/sites-available/motion-videos
server {
  listen 80;
  server_name motion-videos;

  access_log /var/log/nginx/motion-videos.access.log;
  error_log  /var/log/nginx/motion-videos.error.log;

  location /static/ {
    alias <YOUR-PROJECT-DIRECTORY>/app/static/;
  }

  location /videos/ {
    alias <YOUR-PROJECT-DIRECTORY>/app/static/videos/;
  }


  location / {
    proxy_pass http://127.0.0.1:6395;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    client_max_body_size 1M;
  }
}

Assurez-vous d'ajouter l'emplacement de votre projet ici !

Activez ensuite votre serveur :

sudo ln -s /etc/nginx/sites-available/motion-videos /etc/nginx/sites-enabled/motion-videos

Et redémarrez Nginx :

sudo systemctl restart nginx

JQuery pour sélectionner la vidéo

L'application Flask crée une liste de liens vidéo dans le modèle. Lorsque nous cliquons sur un lien vidéo, nous utilisons JQuery pour lancer la nouvelle vidéo comme suit :

  • Obtenir le code du modèle HTML pour l'exécution de la vidéo, qui se trouve dans le modèle 'div'.
  • Remplacer les valeurs de l'info vidéo et de l'URL de la vidéo.
  • Remplacer le code actuel de l'exécution de la vidéo HTML ('div') par le nouveau code de la vidéo HTML .

Le code

Pour notre application Flask , nous utilisons Bootstrap et JQuery. Nous utilisons ici un CDN, mais vous pouvez aussi l'installer localement.

Notre application présente deux colonnes :

  • Sélecteur de date, avec des liens vers les vidéos
  • Fenêtre vidéo

Nous créons deux colonnes pleine hauteur en utilisant flexbox. Nous ne pouvons pas utiliser les colonnes Bootstrap car elles utilisent "float-left", et nous rencontrons également des problèmes avec la longue liste de vidéos. Le sélecteur de date peut contenir plus d'éléments que ce qui peut être affiché à l'écran, ce qui signifie que nous utilisons une colonne 'div' avec "overflow-auto".

Voici le code. Nous lions notre application Flask à 0.0.0.0 , ce qui signifie qu'elle est disponible pour d'autres machines sur notre réseau local. Si vous utilisez un firewall, ouvrez le port pour permettre à d'autres machines de se connecter.

# run.py
from app import main

app = main.create_app()

if __name__ == '__main__':
    app.run(
        host= '0.0.0.0',
        port=6395,
        use_debugger=True,
        use_reloader=True,
    )

La principale chose que nous faisons dans notre application Flask est de sélectionner les dates et les vidéos disponibles.

# main.py
import datetime
import glob
import os
import pathlib
import re

from flask import Flask, request, url_for, redirect, render_template

video_files_subdir = 'videos'
video_files_ext = '.mp4'

class Video:
    def __init__(self, f, d):
        self.d = d
        self.filename = os.path.basename(f)
        if d is None:
            self.page_url = url_for('index', filename=self.filename)
            self.play_url = f'/videos/{self.filename}'
        else:
            self.page_url = url_for('date_dir', date=d, filename=self.filename)
            self.play_url = f'/videos/{d}/{self.filename}'
        name_parts = pathlib.Path(self.filename).stem.split('-')
        self.cam = name_parts[0].split('_')[0]
        self.dt = datetime.datetime.strptime(name_parts[2], '%Y%m%d%H%M%S')
        self.info = self.dt.strftime('%Y-%m-%d') + ' ' + self.filename

def create_app():
    app = Flask(__name__)
    app.jinja_env.auto_reload = True
    app.config['TEMPLATES_AUTO_RELOAD'] = True
    
    def get_dates_videos(date_dir=None):
        # get date_dirs (dates)
        base_dir = os.path.join(app.static_folder, video_files_subdir)
        base_path = pathlib.Path(base_dir)
        date_dirs = [os.path.split(x)[-1] for x in base_path.rglob('????-??-??') if x.is_dir()]
        date_dirs.sort(reverse=True)
        # get files
        current_dir_parts = [base_dir]
        if date_dir is not None:
            current_dir_parts.append(date_dir)
        current_dir_parts.append('*' + video_files_ext)
        path = os.path.join(*current_dir_parts)
        files = glob.glob(path)
        files.sort(key=os.path.getmtime, reverse=True)
        app.logger.info(f'date_dirs = {date_dirs}, files = {files}')
        return dict(
			dates=date_dirs,
			videos=[Video(f, d=date_dir) for f in files],
			date_selected=date_dir,
		)

    @app.route('/')
    def index():
        dates_videos_date = get_dates_videos()
        return render_template('index.html', **dates_videos_date)

    @app.route('/<date>')
    def date_dir(date):
        dates_videos_date = get_dates_videos(date)
        return render_template('index.html', **dates_videos_date)

    return app

Dans le modèle, nous montrons les dates et les vidéos. Les vidéos utilisent un bouton qui déclenche un événement lorsqu'il est cliqué.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Videos</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<style>
body {
	height: 100%;
}
.col-container {
	width: 100vw;
	height: 100vh;
	display: flex;
	flex-direction: row;
}
.col-start {
	min-width: 200px;
}
.col-end {
	position: relative;
	flex: 1;
}
.video-selector {
	max-height: 100%;
	width: 100%;
	position:relative;
	overflow-y:auto;	
}	
</style>
</head>
<body>

{% macro list_videos(videos, filename) %}
	{% if videos and videos|length > 0 %}
		{% for video in videos %}
		<p class="my-0 small">
			{{ video.cam }} &nbsp;
			<button type="button" 
				class="btn btn-link btn-sm my-0 py-0 small video-button" 
				data-video-info="{{ video.info }}"
				data-video-play-url="{{ video.play_url }}">
				{{ video.dt.strftime('%H:%M:%S') }}
			</button>
		</p>
		{% endfor %}
	{% else %} 
		No videos found
	{% endif %}
{% endmacro %}

<div class="col-container">
	<div class="col-start">
		<div class="video-selector p-2">

    {% if dates and dates|length > 0 %}
	<table class="table table-sm">
	<tbody>
		{% if date_selected %}
		<tr><td></td><td>
			<a href="{{ url_for('index') }}">
			&lt;&lt;
			</a>
		</td></tr>
		{% endif %}
	{% for date in dates %}
		{% set date_is_date_selected = false %}
		{% if date_selected and date == date_selected %}
			{% set date_is_date_selected = true %}
		{% endif %}
		<tr>
		<td>
			{{ 'v' if date_is_date_selected else '>' }}
		</td>
		<td>
			<a href="{{ url_for('date_dir', date=date) }}">
			{{ date }}
			</a>
		</td>
		</tr>
		{% if date_is_date_selected %}
		<tr>
		<td>
		</td>
		<td>
			{{ list_videos(videos, filename) }}
		</td>
		</tr>
		{% endif %} 
	{% endfor %}

	{% if date_selected is none %}
	<tr><td colspan="2">
		{{ list_videos(videos, filename) }}
	</td></tr>
	{% endif %} 
	</tbody>
	</table>    
    {% endif %} 

		</div>
	</div>
	<div class="col-end bg-black text-light p-2">
		<div id="video-area"></div>	
	</div>
</div>

<div id="video-area-template" class="visually-hidden">
	<div id="video-area">
		<p class="mb-0">{VIDEO_INFO}</p>
		<div class="embed-responsive embed-responsive-16by9">
			<video class="embed-responsive-item" controls autoplay muted>
				<source src="{VIDEO_PLAY_URL}" type="video/mp4">
			</video>
		</div>
	</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script>
$(document).ready(function(){
	$('.video-button').click(function(){
		var video_info = $(this).data('video-info');
		var video_play_url = $(this).data('video-play-url');
		var video_area_template = $('#video-area-template').html();
		var video_area_data = video_area_template.replace('{VIDEO_INFO}', video_info);
		video_area_data = video_area_data.replace('{VIDEO_PLAY_URL}', video_play_url);
		$('#video-area').replaceWith(video_area_data);
    });
});
</script>

</body>
</html>

Pour démarrer notre application, allez dans le répertoire du projet et tapez :

python run.py

Pointez maintenant votre navigateur sur :

http://motion-videos

Résumé

Il s'agit d'un petit projet qui laisse à désirer, mais qui fonctionne suffisamment bien pour notre objectif. Flask, Jinja, Bootstrap et JQuery sont tout simplement d'incroyables blocs de construction pour créer de petites applications web en très peu de temps.

Liens / crédits

Basic concepts of flexbox
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Basic_concepts_of_flexbox

Flask
https://flask.palletsprojects.com/en/stable

Jinja
https://jinja.palletsprojects.com/en/stable

Print raw HTTP request in Flask or WSGI
https://stackoverflow.com/questions/25466904/print-raw-http-request-in-flask-or-wsgi

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.