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

Simple video galery with Flask, Jinja, Bootstrap and JQuery

View videos by date and time with minimal clicks with Flask, Jinja, Bootstrap and JQuery

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

We have a number of cameras that generate short clips when something moves in front of the camera. All the clips, videos, end up on one system. We use standard programs to view the videos.

But now we want to share these videos with others on our local network, and we don't want to copy the video files. An obvious solution is to select and install something like a video gallery server. But, hey, this is a blog about Python, and this is a small project, so here we are creating this ourselves. We're not implementing HTTPS, or login, which means anyone on the local network can see the videos. If you want, you can restrict access by IP address.

As always, I'm doing this on Ubuntu 22.04, and the other machines on the local network are also running Ubuntu.

Available video galery software vs custom made project

Can we choose something like open source video gallery software? The problem is the required functionality in this case.
We want to minimize the number of clicks to view a video, and to view the next video, etc. This requires an always visible list of videos with the time of the videos, and the list can be long. Most image and video galleries show a page of thumbnails and clicking means a new page opens and we have to go back to select another video. Not what we want!

Summarizing, our screen should look like this:

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

Click on a time and the video is shown and run. Oh, and this does not need to be responsive, we always watch this on a monitor.

The project

This is a perfect task for Flask and Jinja. I already have Nginx running on my development machine. The videos are 'MP4' and are stored in folders, the names of these folders are dates. Flask is only used to select a date and videos. Clicking on a video link does not open a new page but playes the selected video in the video window of the page.

Here is the project directory structure:

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

Create a virtual environment, a project directory, and install Flask.

Nginx

We use Nginx to serve our application:

  • The Flask application, using a reverse proxy server
  • The videos, from a directory
  • The static content, from a directory

Because not all the data is from our Flask application, we assign a hostname (server) to our application:

motion-videos

We edit:

/etc/hosts

and add the line:

127.0.0.1 motion-videos

On other machines in our local network, we also add this line, but now with the IP address of our development machine, the one with the videos and running the application.

Then, to call our application in our browser, we type in the address bar:

http://motion-videos

Now Nginx knows which server we mean, and can also serve the videos and static content.

Here is the Nginx server file, our application is at port 6395, feel free to change this.

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

Make sure to add the location of your project here!

Then enable your server:

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

And restart Nginx:

sudo systemctl restart nginx

JQuery to select the video

The Flask application creates a list of video links in the template. When we click a video link, We use JQuery to run the new video as follows:

  • Get the 'video run HTML template code', this is inside a 'div'
  • Substitute values for the video info and video URL
  • Replace the current video run HTML code ('div') by the new video HTML code

The code

For our Flask application, we use Bootstrap and JQuery. Here we use a CDN, but you can also install this local.

Our application shows two columns:

  • Date selector, with links to the videos
  • Video window

The date selector also shows the times of the videos for a selected date.  We create two full height columns using flexbox. We cannot use Bootstrap columns as they are using 'float-left', and we also run into trouble with the long list of videos. The date selector can have more items than can be displayed on the screen meaning that we use a 'div' with 'overflow-auto'.

Here is the code. We bind our Flask application to 0.0.0.0 meaning that it is available to other machines on our local network. If you use a firewall, then open the port to allow other machines to connect.

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

The main thing we do in our Flask application is selecting the available dates and videos.

# 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 the template we show the dates and videos. The videos use a button that fires an event when clicked.

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

To start our application, go to the project directory and type:

python run.py

Now point your browser at:

http://motion-videos

Summary

This is a small project with much to be desired but it works well enough for our purpose. Flask, Jinja, Bootstrap and JQuery, are simply incredible building blocks to create small web applications in a very short time.

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

Leave a comment

Comment anonymously or log in to comment.

Comments

Leave a reply

Reply anonymously or log in to reply.