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

Eenvoudige videogalerij met Flask, Jinja, Bootstrap en JQuery

Bekijk video's op datum en tijd met een minimum aan klikken met Flask, Jinja, Bootstrap en JQuery

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

We hebben een aantal camera's die korte clips genereren wanneer er iets voor de camera beweegt. Alle clips, video's, komen op één systeem terecht. We gebruiken standaardprogramma's om de video's te bekijken.

Maar nu willen we deze video's delen met anderen op ons lokale netwerk, en we willen de videobestanden niet kopiëren. Een voor de hand liggende oplossing is om iets als een videogalerijserver te selecteren en te installeren. Maar, hé, dit is een blog over Python, en dit is een klein project, dus hier maken we dit zelf. We implementeren HTTPS of login niet, wat betekent dat iedereen op het lokale netwerk de video's kan zien. Als je wilt, kun je de toegang beperken met IP address.

Zoals altijd doe ik dit op Ubuntu 22.04, en de andere machines op het lokale netwerk draaien ook Ubuntu.

Beschikbare videogalerijsoftware vs. zelfgemaakt project

Kunnen we kiezen voor iets als open source videogalerijsoftware? Het probleem is in dit geval de vereiste functionaliteit.
We willen het aantal klikken minimaliseren om een video te bekijken, en om de volgende video te bekijken, enz. Dit vereist een altijd zichtbare lijst van video's met de tijd van de video's, en de lijst kan lang zijn. De meeste afbeeldingen- en videogalerijen tonen een pagina met miniaturen en als we daarop klikken, wordt er een nieuwe pagina geopend en moeten we terug om een andere video te selecteren. Niet wat we willen!

Samengevat zou ons scherm er zo uit moeten zien:

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

Klik op een tijd en de video wordt getoond en uitgevoerd. Oh, en dit hoeft niet responsive te zijn, we bekijken dit altijd op een monitor.

Het project

Dit is een perfecte taak voor Flask en Jinja. Ik heb Nginx al draaien op mijn ontwikkelmachine. De video's zijn 'MP4' en worden opgeslagen in mappen, de namen van deze mappen zijn datums. Flask wordt alleen gebruikt om een datum en video's te selecteren. Als u op een videolink klikt, wordt er geen nieuwe pagina geopend, maar wordt de geselecteerde video afgespeeld in het videovenster van de pagina.

Hier is de directorystructuur van het project:

├── 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

Maak een virtual environment, een projectmap en installeer Flask.

Nginx

We gebruiken Nginx om onze applicatie te serveren:

  • De Flask applicatie, met behulp van een reverse proxy server.
  • De video's, uit een directory
  • De statische inhoud, uit een directory

Omdat niet alle gegevens afkomstig zijn van onze Flask applicatie, wijzen we een hostnaam (server) toe aan onze applicatie:

motion-videos

We bewerken:

/etc/hosts

en voegen de regel toe:

127.0.0.1 motion-videos

Op andere machines in ons lokale netwerk voegen we deze regel ook toe, maar nu met de IP address van onze ontwikkelmachine, degene met de video's en waarop de applicatie draait.

Vervolgens typen we in de adresbalk om onze applicatie in onze browser aan te roepen:

http://motion-videos

Nu weet Nginx welke server we bedoelen en kan het ook de video's en statische inhoud serveren.

Hier is het Nginx serverbestand, onze applicatie staat op poort 6395, verander dit gerust.

# /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;
  }
}

Zorg ervoor dat je hier de locatie van je project toevoegt!

Schakel vervolgens je server in:

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

En start Nginx opnieuw op:

sudo systemctl restart nginx

JQuery om de video te selecteren

De toepassing Flask maakt een lijst met videolinks in de sjabloon. Wanneer we op een videolink klikken, gebruiken we JQuery om de nieuwe video als volgt uit te voeren:

  • Haal de 'video run HTML template code' op, deze staat in een 'div'.
  • Vervang de waarden voor de video-info en video-URL
  • Vervang de huidige videorun HTML code ('div') door de nieuwe video HTML code.

De code

Voor onze Flask toepassing gebruiken we Bootstrap en JQuery. Hier gebruiken we een CDN, maar je kunt deze ook lokaal installeren.

Onze toepassing toont twee kolommen:

  • Datumkiezer, met links naar de video's
  • Videovenster

De datumkiezer toont ook de tijden van de video's voor een geselecteerde datum. We maken twee kolommen van volledige hoogte met flexbox. We kunnen geen kolommen van Bootstrap gebruiken omdat deze 'float-left' gebruiken, en we krijgen ook problemen met de lange lijst met video's. De datumselector kan meer items hebben dan kunnen worden weergegeven op het scherm, wat betekent dat we een 'div' met 'overflow-auto' gebruiken.

Hier is de code. We binden onze Flask applicatie aan 0.0.0.0 , wat betekent dat deze beschikbaar is voor andere machines op ons lokale netwerk. Als je een firewall gebruikt, open dan de poort zodat andere machines verbinding kunnen maken.

# 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,
    )

Het belangrijkste wat we doen in onze Flask applicatie is het selecteren van de beschikbare data en video's. In de sjabloon laten we de data en video's zien.

# 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

In de sjabloon laten we de data en video's zien. De video's gebruiken een knop die een gebeurtenis afvuurt als erop wordt geklikt.

<!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>

Om onze applicatie te starten, gaat u naar de projectmap en typt u:

python run.py

Richt nu uw browser op:

http://motion-videos

Samenvatting

Dit is een klein project dat nog veel te wensen overlaat, maar het werkt goed genoeg voor ons doel. Flask, Jinja, Bootstrap en JQuery zijn gewoon ongelofelijke bouwstenen om in korte tijd kleine webapplicaties te maken.

Links / credits

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.