Простая видеогалерея с Flask, Jinja, Bootstrap и JQuery
Просмотр видео по дате и времени с минимальным количеством кликов с помощью Flask, Jinja, Bootstrap и JQuery
У нас есть несколько камер, которые генерируют короткие ролики, когда что-то движется перед камерой. Все ролики, видео, попадают в одну систему. Для просмотра видео мы используем стандартные программы.
Но теперь мы хотим поделиться этими видео с другими пользователями в нашей локальной сети, и мы не хотим копировать видеофайлы. Очевидное решение - выбрать и установить что-то вроде сервера видеогалереи. Но, эй, это блог о 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 }}
<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>
Чтобы запустить наше приложение, перейдите в каталог проекта и введите:
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
Недавний
- Использование Ingress для доступа к RabbitMQ на кластере Microk8s
- Простая видеогалерея с Flask, Jinja, Bootstrap и JQuery
- Базовое планирование заданий с помощью APScheduler
- Коммутатор базы данных с HAProxy и HAProxy Runtime API
- Docker Swarm rolling updates
- Скрытие первичных ключей базы данных UUID вашего веб-приложения
Большинство просмотренных
- Использование PyInstaller и Cython для создания исполняемого файла Python
- Уменьшение времени отклика на запросы на странице Flask SQLAlchemy веб-сайта
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Подключение к службе на хосте Docker из контейнера Docker
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов