Galería de vídeo simple con Flask, Jinja, Bootstrap y JQuery
Ver vídeos por fecha y hora con un mínimo de clics con Flask, Jinja, Bootstrap y JQuery
Tenemos varias cámaras que generan clips cortos cuando algo se mueve delante de la cámara. Todos los clips, los vídeos, acaban en un sistema. Utilizamos programas estándar para ver los vídeos.
Pero ahora queremos compartir estos vídeos con otras personas de nuestra red local, y no queremos copiar los archivos de vídeo. Una solución obvia es seleccionar e instalar algo como un servidor de galería de vídeos. Pero, hey, este es un blog sobre Python, y este es un pequeño proyecto, así que aquí estamos creando esto nosotros mismos. No estamos implementando HTTPS, o login, lo que significa que cualquier persona en la red local puede ver los videos. Si lo desea, puede restringir el acceso por IP address.
Como siempre, estoy haciendo esto en Ubuntu 22.04, y las otras máquinas en la red local también están ejecutando Ubuntu.
Software de galeria de video disponible vs proyecto a medida
¿Podemos elegir algo como el software de galería de vídeos open source ? El problema es la funcionalidad requerida en este caso.
Queremos minimizar el número de clics para ver un vídeo, y para ver el siguiente vídeo, etc. Esto requiere una lista de vídeos siempre visible con la hora de los vídeos, y la lista puede ser larga. La mayoría de las galerías de imágenes y vídeos muestran una página de miniaturas y hacer clic significa que se abre una nueva página y tenemos que volver atrás para seleccionar otro vídeo. No es lo que queremos.
Resumiendo, nuestra pantalla debería tener este aspecto:
+-------------------+-------------------------------+
| << | |
| > 2024-10-27 | |
| v 2024-10-26 | |
| CAM01 18:54:48 | |
| CAM01 18:53:17 | VIDEO |
| ... | |
| ... | |
| ... | |
| > 2024-10-25 | |
| > 2024-10-24 | |
| ... | |
| | |
| | |
| | |
+-------------------+-------------------------------+
Haga clic en un momento y el vídeo se muestra y se ejecuta. Ah, y esto no necesita ser responsive, siempre vemos esto en un monitor.
El proyecto
Esta es una tarea perfecta para Flask y Jinja. Ya tengo Nginx ejecutándose en mi máquina de desarrollo. Los vídeos son 'MP4' y se almacenan en carpetas, los nombres de estas carpetas son fechas. Flask sólo se utiliza para seleccionar una fecha y vídeos. Al hacer clic en un enlace de vídeo no se abre una nueva página, sino que se reproduce el vídeo seleccionado en la ventana de vídeo de la página.
Esta es la estructura de directorios del proyecto:
├── 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
Crear un virtual environment, un directorio de proyecto, e instalar Flask.
Nginx
Utilizamos Nginx para servir nuestra aplicación:
- La aplicación Flask , utilizando un servidor reverse proxy
- Los vídeos, desde un directorio
- El contenido estático, de un directorio
Como no todos los datos proceden de nuestra aplicación Flask , asignamos un nombre de host (servidor) a nuestra aplicación:
motion-videos
Editamos:
/etc/hosts
y añadimos la línea:
127.0.0.1 motion-videos
En otras máquinas de nuestra red local, también añadimos esta línea, pero ahora con el IP address de nuestra máquina de desarrollo, la que tiene los vídeos y ejecuta la aplicación.
Luego, para llamar a nuestra aplicación en nuestro navegador, escribimos en la barra de direcciones:
http://motion-videos
Ahora Nginx sabe a qué servidor nos referimos, y también puede servir los vídeos y el contenido estático.
Aquí está el archivo del servidor Nginx , nuestra aplicación está en el puerto 6395, siéntete libre de cambiar esto.
# /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;
}
}
¡Asegúrese de añadir la ubicación de su proyecto aquí!
Luego habilite su servidor:
sudo ln -s /etc/nginx/sites-available/motion-videos /etc/nginx/sites-enabled/motion-videos
Y reinicie Nginx:
sudo systemctl restart nginx
JQuery para seleccionar el vídeo
La aplicación Flask crea una lista de enlaces de vídeo en la plantilla. Cuando hacemos clic en un enlace de vídeo, utilizamos JQuery para ejecutar el nuevo vídeo de la siguiente manera:
- Obtener el 'código de la plantilla de ejecución de vídeo HTML ', esto es dentro de un 'div'
- Sustituya los valores de la información del vídeo y la URL del vídeo
- Sustituir el actual código de ejecución de vídeo HTML ('div') por el nuevo código de vídeo HTML
El código
Para nuestra aplicación Flask , utilizamos Bootstrap y JQuery. Aquí usamos un CDN, pero también puedes instalarlo localmente.
Nuestra aplicación muestra dos columnas:
- Selector de fechas, con enlaces a los vídeos
- Ventana de vídeo
El selector de fecha también muestra las horas de los vídeos para una fecha seleccionada. Creamos dos columnas de altura completa utilizando flexbox. No podemos utilizar columnas Bootstrap porque utilizan 'float-left', y además tenemos problemas con la larga lista de vídeos. El selector de fecha puede tener más elementos de los que se pueden mostrar en pantalla, lo que significa que utilizamos un 'div' con 'overflow-auto'.
Este es el código. Vinculamos nuestra aplicación Flask a 0.0.0.0 lo que significa que está disponible para otras máquinas en nuestra red local. Si utilizas un firewall, entonces abre el puerto para permitir que otras máquinas se conecten.
# 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,
)
Lo principal que hacemos en nuestra aplicación Flask es seleccionar las fechas y vídeos 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
En la plantilla mostramos las fechas y los vídeos. Los vídeos utilizan un botón que dispara un evento al pulsarlo.
<!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 }}
<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') }}">
<<
</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>
Para iniciar nuestra aplicación, vaya al directorio del proyecto y escriba:
python run.py
Ahora apunta tu navegador a:
http://motion-videos
Resumen
Este es un pequeño proyecto con mucho que desear, pero funciona lo suficientemente bien para nuestro propósito. Flask, Jinja, Bootstrap y JQuery, son simplemente increíbles bloques de construcción para crear pequeñas aplicaciones web en muy poco tiempo.
Enlaces / créditos
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
Recientes
- Uso de Ingress para acceder a RabbitMQ en un clúster Microk8s
- Galería de vídeo simple con Flask, Jinja, Bootstrap y JQuery
- Programación básica de trabajos con APScheduler
- Un conmutador de base de datos con HAProxy y el HAProxy Runtime API
- Docker Swarm rolling updates
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
Más vistos
- Usando PyInstaller y Cython para crear un ejecutable de Python
- Reducir los tiempos de respuesta de las páginas de un sitio Flask SQLAlchemy web
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados