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

Простая видеогалерея с Flask, Jinja, Bootstrap и JQuery

Просмотр видео по дате и времени с минимальным количеством кликов с помощью Flask, Jinja, Bootstrap и JQuery

28 октября 2024
В Flask
post main image
https://www.pexels.com/nl-nl/@mo3ath-photos-110226063

У нас есть несколько камер, которые генерируют короткие ролики, когда что-то движется перед камерой. Все ролики, видео, попадают в одну систему. Для просмотра видео мы используем стандартные программы.

Но теперь мы хотим поделиться этими видео с другими пользователями в нашей локальной сети, и мы не хотим копировать видеофайлы. Очевидное решение - выбрать и установить что-то вроде сервера видеогалереи. Но, эй, это блог о Python, и это небольшой проект, так что здесь мы создаем его сами. Мы не реализуем HTTPS, не входим в систему, что означает, что любой человек в локальной сети может посмотреть видео. Если вы хотите, вы можете ограничить доступ с помощью IP address.

Как обычно, я делаю это на Ubuntu 22.04, а другие машины в локальной сети также работают под управлением Ubuntu.

Доступное программное обеспечение для создания видеогалереи против проекта, сделанного на заказ

Можем ли мы выбрать что-то вроде программного обеспечения для видеогалереи open source ? Проблема заключается в требуемом функционале в данном случае.
Мы хотим минимизировать количество кликов для просмотра видео, а также для просмотра следующего видео и т. д. Для этого требуется постоянно видимый список видео с указанием времени просмотра, причем список может быть длинным. Большинство галерей изображений и видео показывают страницу миниатюр, и щелчок означает, что открывается новая страница и нам приходится возвращаться назад, чтобы выбрать другое видео. Не то, что нам нужно!

Подводя итог, можно сказать, что наш экран должен выглядеть следующим образом:

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

Щелкните по времени, и видео будет показано и запущено. О, и это не обязательно должно быть отзывчивым, мы всегда смотрим это на мониторе.

Проект

Это идеальное задание для Flask и Jinja. У меня уже есть Nginx , запущенный на моей машине разработки. Видеоролики имеют формат 'MP4' и хранятся в папках, имена этих папок - даты. Flask используется только для выбора даты и видео. При нажатии на ссылку видео не открывается новая страница, а воспроизводится выбранное видео в видеоокне страницы.

Вот структура каталогов проекта:

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

Создайте каталог virtual environment, каталог проекта и установите Flask.

Nginx

Мы используем Nginx для обслуживания нашего приложения:

  • Приложение Flask , использующее сервер reverse proxy .
  • Видео, из каталога
  • Статический контент, из каталога

Поскольку не все данные поступают из нашего приложения Flask , мы присваиваем нашему приложению имя хоста (сервера):

motion-videos

Редактируем:

/etc/hosts

и добавляем строку:

127.0.0.1 motion-videos

На других машинах в нашей локальной сети мы также добавляем эту строку, но теперь с IP address нашей машины разработки, той, на которой есть видео и запущено приложение.

Затем, чтобы вызвать наше приложение в браузере, мы набираем в адресной строке:

http://motion-videos

Теперь Nginx знает, какой сервер мы имеем в виду, и может также обслуживать видео и статический контент.

Вот файл сервера Nginx , наше приложение находится на порту 6395, не стесняйтесь изменить это.

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

Не забудьте добавить сюда местоположение вашего проекта!

Затем включите ваш сервер:

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

И перезапустите Nginx:

sudo systemctl restart nginx

JQuery для выбора видео

Приложение Flask создает список ссылок на видео в шаблоне. Когда мы нажимаем на ссылку на видео, мы используем JQuery для запуска нового видео следующим образом:

  • Получаем код шаблона 'video run HTML ', он находится внутри 'div'
  • Подставьте значения для информации о видео и URL видео
  • Замените текущий код видеозапуска HTML ('div') на новый код видеозапуска HTML

Код

Для нашего приложения Flask мы используем Bootstrap и JQuery. Здесь мы используем CDN, но вы также можете установить его локально.

Наше приложение отображает две колонки:

  • Селектор даты со ссылками на видео
  • окно видео

Селектор даты также показывает время видео для выбранной даты. Мы создаем две колонки во всю высоту, используя flexbox. Мы не можем использовать столбцы Bootstrap , так как они используют 'float-left', и мы также столкнемся с проблемой длинного списка видео. В селекторе дат может быть больше элементов, чем может быть отображено на экране, поэтому мы используем 'div' с 'overflow-auto'.

Вот код. Мы привязываем наше приложение Flask к 0.0.0.0 , что означает, что оно доступно для других машин в нашей локальной сети. Если вы используете firewall, то откройте порт, чтобы другие машины могли подключаться.

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

Главное, что мы делаем в нашем приложении Flask , - это выбираем доступные даты и видео.

# 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

В шаблоне мы показываем даты и видео. Для видео используется кнопка, при нажатии на которую происходит событие.

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

Чтобы запустить наше приложение, перейдите в каталог проекта и введите:

python run.py

Теперь наведите браузер на:

http://motion-videos

Резюме .

Это небольшой проект, в котором есть к чему стремиться, но он достаточно хорошо работает для нашей цели. Flask, Jinja, Bootstrap и JQuery - это просто невероятные строительные блоки для создания небольших веб-приложений за очень короткое время.

Ссылки / кредиты

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

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.