first commit
This commit is contained in:
commit
d746f5b541
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
data/uploads/*
|
||||
data/slideshow.json
|
||||
45
README.md
Normal file
45
README.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Reclame
|
||||
|
||||
## About
|
||||
Use a RaspberryPi to show a full-screen Slideshow (Kiosk-mode)
|
||||
|
||||
## Installation TL;DR
|
||||
```bash
|
||||
sudo apt-get update && sudo apt-get dist-upgrade
|
||||
sudo apt-get install git chromium-browser -y
|
||||
|
||||
git clone https://github.com/jr-k/reclame.git
|
||||
cd reclame && pip3 install -r requirements.txt && cp data/slideshow.json.dist data/slideshow.json
|
||||
./reclame.py
|
||||
```
|
||||
|
||||
## Installation - step by step
|
||||
### Basic Setup
|
||||
For basic RaspberryPi setup you can use most of the available guides, for example this one:
|
||||
https://gist.github.com/blackjid/dfde6bedef148253f987
|
||||
|
||||
### HDMI Mode
|
||||
You may need to set the HDMI Mode on the raspi to ensure the hdmi resolution matches your screen exactly. Here is the official documentation:
|
||||
https://www.raspberrypi.org/documentation/configuration/config-txt/video.md
|
||||
|
||||
However, I used this one: `(2,82) = 1920x1080 60Hz 1080p`
|
||||
|
||||
### Installation of base software
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get upgrade
|
||||
sudo apt-get dist-upgrade
|
||||
sudo apt-get install git chromium-browser -y
|
||||
|
||||
git clone https://github.com/jr-k/reclame.git
|
||||
cd reclame && pip3 install -r requirements.txt && cp data/slideshow.json.dist data/slideshow.json
|
||||
./reclame.py
|
||||
```
|
||||
|
||||
## Prepare your Slideshow
|
||||
Everything slideshow-related happens in the ./data/uploads folder.
|
||||
- Put some images into the /data/uploads folder. Ideally with the same resultion of the screen (eg. 1920x1080px).
|
||||
- Edit the slideshow.json
|
||||
|
||||
## You are done now :)
|
||||
If everything is set up correctly, the RaspberryPi shall start chromium in fullscreen directly after bootup and after some seconds of showing the date & time (default.html) your slideshow shall start and loop endlessly.
|
||||
1
data/404.html
Executable file
1
data/404.html
Executable file
@ -0,0 +1 @@
|
||||
404 Not found
|
||||
4
data/slideshow.json.dist
Executable file
4
data/slideshow.json.dist
Executable file
@ -0,0 +1,4 @@
|
||||
[
|
||||
{"location":"https://ffmpeg.org","delay":10,"type":"url"},
|
||||
{"location":"https://unix.org","delay":20,"type":"url"},
|
||||
]
|
||||
67
reclame.py
Executable file
67
reclame.py
Executable file
@ -0,0 +1,67 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
# <server>
|
||||
app = Flask(__name__, template_folder='views', static_folder='data')
|
||||
port = 5000
|
||||
# </server>
|
||||
|
||||
# <xenv>
|
||||
destination_path = '/home/pi/.config/lxsession/LXDE-pi/autostart'
|
||||
os.makedirs(os.path.dirname(destination_path), exist_ok=True)
|
||||
xenv_presets = f"""
|
||||
@lxpanel --profile LXDE-pi
|
||||
@pcmanfm --desktop --profile LXDE-pi
|
||||
@xscreensaver -no-splash
|
||||
#@point-rpi
|
||||
@xset s off
|
||||
@xset -dpms
|
||||
@xset s noblank
|
||||
@unclutter -display :0 -noevents -grab
|
||||
@sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' ~/.config/chromium/Default/Preferences
|
||||
@chromium-browser --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --disable-restore-session-state --noerrdialogs --kiosk --incognito --window-position=0,0 --display=:0 http://localhost:{port}
|
||||
"""
|
||||
with open(destination_path, 'w') as file:
|
||||
file.write(xenv_presets)
|
||||
# </xenv>
|
||||
|
||||
# <utils>
|
||||
def get_ip_address():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ip", "-4", "route", "get", "8.8.8.8"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
ip_address = result.stdout.split()[6]
|
||||
return ip_address
|
||||
except Exception as e:
|
||||
print(f"Error obtaining IP address: {e}")
|
||||
return 'Unknown'
|
||||
# </utils>
|
||||
|
||||
# <web>
|
||||
@app.route('/')
|
||||
def index():
|
||||
with open('./data/slideshow.json', 'r') as file:
|
||||
items = json.load(file)
|
||||
|
||||
return render_template('player.jinja.html', port=port, items=json.dumps(items))
|
||||
|
||||
@app.route('/slide/default')
|
||||
def slide_default():
|
||||
return render_template('default.jinja.html', ipaddr=get_ip_address())
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
return send_from_directory('data', '404.html'), 404
|
||||
# </web>
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=port)
|
||||
1
requirements.txt
Executable file
1
requirements.txt
Executable file
@ -0,0 +1 @@
|
||||
flask
|
||||
64
views/default.jinja.html
Executable file
64
views/default.jinja.html
Executable file
@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
function updateTime() {
|
||||
var date = new Date();
|
||||
var hours = (date.getHours() < 10 ? '0' : '') + date.getHours();
|
||||
var minutes = (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
|
||||
var seconds = (date.getSeconds() < 10 ? '0' : '') + date.getSeconds();
|
||||
var dayInMonth = date.getDate();
|
||||
var month = date.getMonth();
|
||||
var year = date.getFullYear();
|
||||
var day = date.getDay();
|
||||
var dayLabels = ["Dimanche", "Lundi", "Mardis", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
|
||||
var monthLabels = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"];
|
||||
|
||||
var timeLabel = hours + ":" + minutes;
|
||||
var dateLabel = dayLabels[day] + " " + dayInMonth + " " + monthLabels[month] + " " + year;
|
||||
|
||||
document.getElementById('time').innerHTML = timeLabel;
|
||||
document.getElementById('date').innerHTML = dateLabel;
|
||||
setTimeout(updateTime, 1000);
|
||||
}
|
||||
|
||||
window.addEventListener("load", updateTime);
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
text-align: center;
|
||||
font-family: 'Arial', 'sans-serif';
|
||||
color: white;
|
||||
background-color: black
|
||||
}
|
||||
#bottom {
|
||||
background: #111;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 20px 0;
|
||||
}
|
||||
#time {
|
||||
font-size: 10em;
|
||||
}
|
||||
#date {
|
||||
font-size: 3em;
|
||||
}
|
||||
#ipaddr {
|
||||
font-size: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="time"></div>
|
||||
<div id="date"></div>
|
||||
<div id="bottom">
|
||||
<div id="ipaddr"></div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('ipaddr').innerText = '{{ ipaddr|safe }}';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
131
views/player.jinja.html
Executable file
131
views/player.jinja.html
Executable file
@ -0,0 +1,131 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<title>
|
||||
Reclame
|
||||
</title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slide {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: black;
|
||||
}
|
||||
|
||||
.slide, iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
padding-top: 0;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
}
|
||||
|
||||
.slide iframe {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.slide img {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="FirstSlide" class="slide" style="visibility: hidden;">
|
||||
<iframe src="/slide/default"></iframe>
|
||||
</div>
|
||||
<div id="SecondSlide" style="visibility: visible;">
|
||||
<iframe src="/slide/default"></iframe>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var items = {{ items|safe }};
|
||||
var delay = 3000 / 1;
|
||||
var curUrl = 0;
|
||||
preloadIntoFirstSlide();
|
||||
|
||||
function loadUrl(element, callbackReady) {
|
||||
element.innerHTML = `<iframe src="${items[curUrl].location}"></iframe>`;
|
||||
callbackReady();
|
||||
}
|
||||
|
||||
function loadPicture(element, callbackReady) {
|
||||
element.innerHTML = `<img src="${items[curUrl].location}" alt="" />`;
|
||||
callbackReady();
|
||||
}
|
||||
|
||||
function loadVideo(element, callbackReady) {
|
||||
element.innerHTML = `<video autoplay controls><source src=${items[curUrl].location} type="video/mp4" /></video>`;
|
||||
var video = element.querySelector('video');
|
||||
video.addEventListener('loadedmetadata', function() {
|
||||
items[curUrl].delay = Math.ceil(video.duration);
|
||||
console.log('change delay to ', video.duration)
|
||||
console.log(items[curUrl]);
|
||||
callbackReady();
|
||||
});
|
||||
}
|
||||
|
||||
function preloadIntoFirstSlide() {
|
||||
var element = document.getElementById('FirstSlide');
|
||||
var callbackReady = function() {
|
||||
setTimeout("moveToFirstSlide()", delay);
|
||||
};
|
||||
|
||||
switch (items[curUrl].type) {
|
||||
case 'url': loadUrl(element, callbackReady); break;
|
||||
case 'picture': loadPicture(element, callbackReady); break;
|
||||
case 'video': loadVideo(element, callbackReady); break;
|
||||
default: loadUrl(element, callbackReady); break;
|
||||
}
|
||||
}
|
||||
|
||||
function moveToFirstSlide() {
|
||||
console.log(items[curUrl]);
|
||||
delay = items[curUrl].delay * 1000;
|
||||
curUrl = (curUrl + 1) === items.length ? 0 : curUrl + 1;
|
||||
document.querySelector('#FirstSlide').style.visibility = 'visible';
|
||||
document.querySelector('#SecondSlide').style.visibility = 'hidden';
|
||||
preloadIntoSecondSlide();
|
||||
}
|
||||
|
||||
function preloadIntoSecondSlide() {
|
||||
var element = document.getElementById('SecondSlide');
|
||||
var callbackReady = function() {
|
||||
setTimeout("moveToSecondSlide()", delay);
|
||||
};
|
||||
|
||||
switch (items[curUrl].type) {
|
||||
case 'url': loadUrl(element, callbackReady); break;
|
||||
case 'picture': loadPicture(element, callbackReady); break;
|
||||
case 'video': loadVideo(element, callbackReady); break;
|
||||
default: loadUrl(element, callbackReady); break;
|
||||
}
|
||||
}
|
||||
|
||||
function moveToSecondSlide() {
|
||||
delay = items[curUrl].delay * 1000;
|
||||
curUrl = (curUrl + 1) === items.length ? 0 : curUrl + 1;
|
||||
document.querySelector('#FirstSlide').style.visibility = 'hidden';
|
||||
document.querySelector('#SecondSlide').style.visibility = 'visible';
|
||||
preloadIntoFirstSlide();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user