Compare commits
10 Commits
2aa965cc83
...
dacad4db10
Author | SHA1 | Date |
---|---|---|
xoy | dacad4db10 | |
EbenKouao | 1228e5e5f0 | |
EbenKouao | 870fc34065 | |
Floris | 71e26f8b5d | |
Floris | 7c940fba67 | |
Eben Kouao | 9b27f5d8b4 | |
Eben Kouao | 9accc34541 | |
Eben | e2b102b208 | |
Eben | fc4c270bda | |
Eben | 2b52cf7927 |
|
@ -0,0 +1,4 @@
|
||||||
|
__pycache__/
|
||||||
|
test.py
|
||||||
|
static/images/*
|
||||||
|
venv/
|
73
README.md
73
README.md
|
@ -1,79 +1,38 @@
|
||||||
# Make you own Raspberry Pi Camera Stream
|
# Flask Camera Live Stream
|
||||||
|
|
||||||
Create your own live stream from a Raspberry Pi using the Pi camera module. Build your own applications from here.
|
[Original Project](https://github.com/EbenKouao/pi-camera-stream-flask)
|
||||||
|
|
||||||
## How it works
|
## Usage
|
||||||
The Pi streams the output of the camera module over the web via Flask. Devices connected to the same network would be able to access the camera stream via
|
|
||||||
|
### Creating the virtual environment
|
||||||
|
|
||||||
```
|
```
|
||||||
<raspberry_pi_ip:5000>
|
pip install venv
|
||||||
|
python -m venv venv
|
||||||
```
|
```
|
||||||
|
|
||||||
## Screenshots
|
### Activate the virtual environment
|
||||||
| ![Setup](readme/pi-stream-client.jpg) | ![Live Pi Camera Stream](readme/pi-stream-screen-capture.jpg) |
|
|
||||||
|---|---|
|
|
||||||
| Pi Setup | Pi - Live Stream |
|
|
||||||
|
|
||||||
## Preconditions
|
*Windows (CMD)*
|
||||||
|
|
||||||
* Raspberry Pi 4, 2GB is recommended for optimal performance. However you can use a Pi 3 or older, you may see a increase in latency.
|
|
||||||
* Raspberry Pi 4 Camera Module or Pi HQ Camera Module (Newer version)
|
|
||||||
* Python 3 recommended.
|
|
||||||
|
|
||||||
## Library dependencies
|
|
||||||
Install the following dependencies to create camera stream.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt-get install libatlas-base-dev
|
venv\Scripts\activate.bat
|
||||||
sudo apt-get install libjasper-dev
|
|
||||||
sudo apt-get install libqtgui4
|
|
||||||
sudo apt-get install libqt4-test
|
|
||||||
sudo apt-get install libhdf5-dev
|
|
||||||
|
|
||||||
sudo pip3 install flask
|
|
||||||
sudo pip3 install numpy
|
|
||||||
sudo pip3 install opencv-contrib-python
|
|
||||||
sudo pip3 install imutils
|
|
||||||
sudo pip3 install opencv-python
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
pip3 install opencv-python
|
*Linux / Mac OS*
|
||||||
|
|
||||||
## Step 1 – Cloning Raspberry Pi Camera Stream
|
|
||||||
Open up terminal and clone the Camera Stream repo:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cd /home/pi
|
source venv/bin/activate
|
||||||
git clone https://github.com/EbenKouao/pi-camera-stream-flask.git
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 2 – Launch Web Stream
|
### Installing the dependencies
|
||||||
|
|
||||||
Note: Creating an Autostart of the main.py script is recommended to keep the stream running on bootup.
|
|
||||||
```bash cd modules
|
|
||||||
sudo python3 /home/pi/pi-camera-stream-flask/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 3 – Autostart your Pi Stream
|
|
||||||
|
|
||||||
Optional: A good idea is running your Pi Camera stream at Pi boot up. This removes the need to re-run the script every time you want to create the stream.
|
|
||||||
You can do this by going adding the boot up code the .bashrc file.
|
|
||||||
|
|
||||||
Via the Desktop GUI - right click in your /home/pi/ directory -> show hidden -> open .bashrc and add the code.
|
|
||||||
Or alternatively access via terminal:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo nano /home/pi/.bashrc
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Go the end of the and add the following (from above):
|
### Running the app (not recommended for production)
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo python3 /home/pi/pi-camera-stream-flask/main.py
|
python main.py
|
||||||
```
|
```
|
||||||
This would cause the following terminal command to auto-start upon Raspberry Pi boot up.
|
|
||||||
|
|
||||||
## Download Beta image of Raspberry Pi Camera Stream
|
|
||||||
Any troubles installing, try out the already compiled Raspberry Pi (Raspbian OS) Image of [Raspberry Pi Camera Stream](https://smartbuilds.io).
|
|
||||||
![Raspbian Camera Stream Image](img/readme/[].png)
|
|
||||||
|
|
42
camera.py
42
camera.py
|
@ -1,21 +1,17 @@
|
||||||
#Modified by smartbuilds.io
|
|
||||||
#Date: 27.09.20
|
|
||||||
#Desc: This scrtipt script..
|
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
from imutils.video.pivideostream import PiVideoStream
|
from datetime import datetime
|
||||||
import imutils
|
|
||||||
import time
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
class VideoCamera(object):
|
class VideoCamera(object):
|
||||||
def __init__(self, flip = False):
|
def __init__(self, flip=False, file_type=".jpg", photo_string="stream_photo"):
|
||||||
self.vs = PiVideoStream().start()
|
self.vs = cv2.VideoCapture(0)
|
||||||
self.flip = flip
|
self.flip = flip
|
||||||
time.sleep(2.0)
|
self.file_type = file_type
|
||||||
|
self.photo_string = photo_string
|
||||||
|
self.exposure_value = self.vs.get(cv2.CAP_PROP_EXPOSURE)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.vs.stop()
|
self.vs.release()
|
||||||
|
|
||||||
def flip_if_needed(self, frame):
|
def flip_if_needed(self, frame):
|
||||||
if self.flip:
|
if self.flip:
|
||||||
|
@ -23,6 +19,26 @@ class VideoCamera(object):
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
def get_frame(self):
|
def get_frame(self):
|
||||||
frame = self.flip_if_needed(self.vs.read())
|
ret, frame = self.vs.read()
|
||||||
ret, jpeg = cv2.imencode('.jpg', frame)
|
if not ret:
|
||||||
|
return None
|
||||||
|
|
||||||
|
frame = self.flip_if_needed(frame)
|
||||||
|
ret, jpeg = cv2.imencode(self.file_type, frame)
|
||||||
return jpeg.tobytes()
|
return jpeg.tobytes()
|
||||||
|
|
||||||
|
def take_picture(self):
|
||||||
|
ret, frame = self.vs.read()
|
||||||
|
if not ret:
|
||||||
|
return
|
||||||
|
|
||||||
|
frame = self.flip_if_needed(frame)
|
||||||
|
today_date = datetime.now().strftime("%m%d%Y-%H%M%S")
|
||||||
|
cv2.imwrite(str("static/images/" + self.photo_string + "_" + today_date + self.file_type), frame)
|
||||||
|
|
||||||
|
def set_exposure(self, exposure_value):
|
||||||
|
self.vs.set(cv2.CAP_PROP_EXPOSURE, exposure_value)
|
||||||
|
self.exposure_value = exposure_value
|
||||||
|
|
||||||
|
def get_exposure(self):
|
||||||
|
return self.exposure_value
|
||||||
|
|
50
main.py
50
main.py
|
@ -1,25 +1,26 @@
|
||||||
#Modified by smartbuilds.io
|
from flask import Flask, render_template, Response
|
||||||
#Date: 27.09.20
|
|
||||||
#Desc: This web application serves a motion JPEG stream
|
|
||||||
# main.py
|
|
||||||
# import the necessary packages
|
|
||||||
from flask import Flask, render_template, Response, request
|
|
||||||
from camera import VideoCamera
|
from camera import VideoCamera
|
||||||
import time
|
from util import list_files_in_dir, generate_url
|
||||||
import threading
|
|
||||||
import os
|
|
||||||
|
|
||||||
pi_camera = VideoCamera(flip=False) # flip pi camera if upside down.
|
camera = VideoCamera(flip=False)
|
||||||
|
|
||||||
# App Globals (do not edit)
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template('index.html') #you can customze index.html here
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/images')
|
||||||
|
def images_view():
|
||||||
|
file_directory = 'images'
|
||||||
|
url_list = list()
|
||||||
|
|
||||||
|
for file in list_files_in_dir('static/'+file_directory):
|
||||||
|
url_list.append(generate_url(file_directory, file))
|
||||||
|
|
||||||
|
return render_template('images.html', urls=url_list)
|
||||||
|
|
||||||
def gen(camera):
|
def gen(camera):
|
||||||
#get camera frame
|
|
||||||
while True:
|
while True:
|
||||||
frame = camera.get_frame()
|
frame = camera.get_frame()
|
||||||
yield (b'--frame\r\n'
|
yield (b'--frame\r\n'
|
||||||
|
@ -27,12 +28,25 @@ def gen(camera):
|
||||||
|
|
||||||
@app.route('/video_feed')
|
@app.route('/video_feed')
|
||||||
def video_feed():
|
def video_feed():
|
||||||
return Response(gen(pi_camera),
|
return Response(gen(camera),
|
||||||
mimetype='multipart/x-mixed-replace; boundary=frame')
|
mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||||
|
|
||||||
|
@app.route('/picture')
|
||||||
|
def take_picture():
|
||||||
|
camera.take_picture()
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
@app.route('/moreexposure')
|
||||||
|
def more_exposure():
|
||||||
|
exposure = camera.get_exposure()
|
||||||
|
camera.set_exposure(exposure + 1)
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
@app.route('/lessexposure')
|
||||||
|
def less_exposure():
|
||||||
|
exposure = camera.get_exposure()
|
||||||
|
camera.set_exposure(exposure - 1)
|
||||||
|
return "None"
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
app.run(host='0.0.0.0', debug=False)
|
app.run(host='0.0.0.0', debug=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
Binary file not shown.
Before Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1,10 @@
|
||||||
|
blinker==1.6.2
|
||||||
|
click==8.1.7
|
||||||
|
colorama==0.4.6
|
||||||
|
Flask==2.3.3
|
||||||
|
itsdangerous==2.1.2
|
||||||
|
Jinja2==3.1.2
|
||||||
|
MarkupSafe==2.1.3
|
||||||
|
numpy==1.25.2
|
||||||
|
opencv-python==4.8.0.76
|
||||||
|
Werkzeug==2.3.7
|
|
@ -0,0 +1,45 @@
|
||||||
|
$(function() {
|
||||||
|
$('a#take-picture').on('click', function(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
$.getJSON('/picture',
|
||||||
|
function(data) {
|
||||||
|
//do nothing
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
$('a#more-exposure').on('click', function(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
$.getJSON('/moreexposure',
|
||||||
|
function(data) {
|
||||||
|
//do nothing
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
$('a#less-exposure').on('click', function(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
$.getJSON('/lessexposure',
|
||||||
|
function(data) {
|
||||||
|
//do nothing
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
$('a#copy-video-stream-url').on('click', function(e) {
|
||||||
|
const textArea = document.createElement("textarea");
|
||||||
|
const video_stream_path = document.getElementById("bg").src;
|
||||||
|
textArea.value = video_stream_path;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,84 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: black;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
overflow: hidden;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
background-color: black;
|
||||||
|
opacity:0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar div {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar a {
|
||||||
|
float: left;
|
||||||
|
display: block;
|
||||||
|
color: #f2f2f2;
|
||||||
|
text-align: center;
|
||||||
|
padding: 14px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar a.active {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.fa {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 60px;
|
||||||
|
box-shadow: 0px 0px 2px #888;
|
||||||
|
padding: 0.5em 0.6em;
|
||||||
|
background: blue;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 35%
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: Transparent;
|
||||||
|
background-repeat:no-repeat;
|
||||||
|
border: none;
|
||||||
|
cursor:pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
outline:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-bg {
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
max-height: 100vh;
|
||||||
|
max-width: 100vh;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Camera Live Feed - Images</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% for url in urls %}
|
||||||
|
<img src="{{ url }}" alt="Taken Image">
|
||||||
|
{% endfor %}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,177 +1,62 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta charset="UTF-8">
|
||||||
<style>
|
<title>Camera Live Feed</title>
|
||||||
body {
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
margin: 0;
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
}
|
</head>
|
||||||
|
|
||||||
.navbar {
|
<body>
|
||||||
overflow: hidden;
|
<div class="main">
|
||||||
position: fixed;
|
<img class="camera-bg" id="bg" class="center" src="{{ url_for('video_feed') }}">
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
margin: auto;
|
|
||||||
background-color: black;
|
|
||||||
opacity:0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar a {
|
|
||||||
float: left;
|
|
||||||
display: block;
|
|
||||||
color: #f2f2f2;
|
|
||||||
text-align: center;
|
|
||||||
padding: 14px 16px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar a:hover {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar a.active {
|
|
||||||
background-color: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
padding: 16px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.camera-movement{
|
|
||||||
float: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lights-button{
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
i.fa {
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 60px;
|
|
||||||
box-shadow: 0px 0px 2px #888;
|
|
||||||
padding: 0.5em 0.6em;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
display: block;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
width: 35%
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: Transparent;
|
|
||||||
background-repeat:no-repeat;
|
|
||||||
border: none;
|
|
||||||
cursor:pointer;
|
|
||||||
overflow: hidden;
|
|
||||||
outline:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
//CSS
|
|
||||||
.camera-bg {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
/* Preserve aspet ratio */
|
|
||||||
min-width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
|
|
||||||
/* Full height */
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
|
|
||||||
/* Center and scale the image nicely */
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-right-logo {
|
|
||||||
position: absolute;
|
|
||||||
top: 3%;
|
|
||||||
left: 2%;
|
|
||||||
font-size: 38px;
|
|
||||||
color: white;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<title>Make - PiStream</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="main" id="newpost">
|
|
||||||
<img class="camera-bg" style="width: 100%; height:80%; background-attachment: fixed;" id="bg" class="center" src="{{ url_for('video_feed') }}">
|
|
||||||
<!--<img class="camera-bg" style="width: 100%; height:80%; background-attachment: fixed;" id="bg" class="center" src="https://www.psdbox.com/wp-content/uploads/2011/01/security-camera-photoshop-effect.jpg">-->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="top-right-logo">
|
|
||||||
<a></a>Raspberry Pi - Camera Stream </a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar">
|
||||||
<div class="navbar">
|
<div>
|
||||||
|
<a href="/images" title="Gallery">
|
||||||
<div class="ignoreCall">
|
<button>
|
||||||
<a id=decline class="but_def">
|
<i class="fa fa-picture-o fa-2x" aria-hidden="true"></i>
|
||||||
<button id="button">
|
|
||||||
<i style="background: red; color: white;" class="fa fa-times fa-2x" aria-hidden="true"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<div>
|
||||||
|
<a href="#" id="take-picture" title="Take a picture">
|
||||||
|
<button>
|
||||||
|
<i class="fa fa-camera fa-2x" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="#" id="more-exposure" title="More exposure">
|
||||||
|
<button>
|
||||||
|
<i class="fa fa-plus fa-2x" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="#" id="less-exposure" title="Less exposure">
|
||||||
|
<button>
|
||||||
|
<i class="fa fa-minus fa-2x" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
|
<div>
|
||||||
|
<a href="#" id="copy-video-stream-url" title="Copy the video stream url">
|
||||||
|
<button>
|
||||||
|
<i class="fa fa-clipboard fa-2x" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
</div>
|
||||||
|
|
||||||
var button = document.getElementById('button');
|
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="{{ url_for('static', filename='script.js') }}"></script>
|
||||||
button.onclick = function() {
|
</body>
|
||||||
var div = document.getElementById('newpost');
|
|
||||||
if (div.style.display !== 'none') {
|
|
||||||
div.style.display = 'none';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
div.style.display = 'block';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import os
|
||||||
|
from flask import url_for
|
||||||
|
|
||||||
|
def list_files_in_dir(directory_path: str) -> list[str]:
|
||||||
|
file_list = os.listdir(directory_path)
|
||||||
|
|
||||||
|
file_list = [file for file in file_list if os.path.isfile(os.path.join(directory_path, file))]
|
||||||
|
|
||||||
|
return file_list
|
||||||
|
|
||||||
|
def generate_url(directory_path: str, file_name: str) -> str:
|
||||||
|
url = url_for('static', filename=f'{directory_path}/{file_name}')
|
||||||
|
return url
|
Loading…
Reference in New Issue