This commit is contained in:
jr-k 2024-08-12 14:19:55 +02:00
commit 4712047015
21 changed files with 260 additions and 77 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -37,8 +37,6 @@ jQuery(document).ready(function ($) {
'padding-top': '0px'
});
function createElement() {
let screen = $('#screen');
let screenWidth = screen.width();
@ -57,7 +55,7 @@ jQuery(document).ready(function ($) {
y = Math.round(Math.max(0, Math.min(y, screenHeight - elementHeight)));
let elementId = elementCounter++;
let element = $('<div class="element" id="element-' + elementId + '" data-id="' + elementId + '"><button>Button</button></div>');
let element = $('<div class="element" id="element-' + elementId + '" data-id="' + elementId + '"><i class="fa fa-cog"></i></div>');
// let element = $('<div class="element" id="' + elementId + '"><button>Button</button><div class="rotate-handle"></div></div>');
element.css({
@ -106,6 +104,8 @@ jQuery(document).ready(function ($) {
setTimeout(function() {
focusElement(element);
}, 10);
return element;
}
$(document).on('click', '.element-list-item', function(){
@ -113,7 +113,9 @@ jQuery(document).ready(function ($) {
})
$(document).on('click', '.remove-element', function(){
removeElementById($(this).attr('data-id'));
if (confirm(l.js_common_are_you_sure)) {
removeElementById($(this).attr('data-id'));
}
})
function removeElementById(elementId) {
@ -122,8 +124,16 @@ jQuery(document).ready(function ($) {
}
function addElementToList(elementId) {
let listItem = $('<div class="element-list-item" data-id="' + elementId + '">Element ' + elementId + ' <button type="button" class="remove-element" data-id="' + elementId + '">remove</button></div>');
$('#elementList').append(listItem);
const listItem = `<div class="element-list-item" data-id="__ID__">
Element __ID__
<button type="button" class="btn btn-neutral configure-element content-explr-picker" data-id="__ID__">
<i class="fa fa-cog"></i>
</button>
<button type="button" class="btn btn-naked remove-element" data-id="__ID__">
<i class="fa fa-trash"></i>
</button>
</div>`;
$('#elementList').append($(listItem.replace(/__ID__/g, elementId)));
updateZIndexes();
}
@ -145,9 +155,11 @@ jQuery(document).ready(function ($) {
function updateForm(element) {
if (!element) {
$('form#elementForm input').val('').prop('disabled', true);
$('.form-element-properties').addClass('hidden');
return;
}
$('.form-element-properties').removeClass('hidden');
$('form#elementForm input').prop('disabled', false);
let offset = element.position();
@ -158,6 +170,8 @@ jQuery(document).ready(function ($) {
$('#elem-width').val(element.width());
$('#elem-height').val(element.height());
}
$(element).find('i').css('font-size', Math.min(element.width(), element.height()) / 3);
/*
let rotation = element.css('transform');
let values = rotation.split('(')[1].split(')')[0].split(',');
@ -201,13 +215,19 @@ jQuery(document).ready(function ($) {
}
});
$(document).on('click', '#addElement', function () {
createElement();
});
// $(document).on('click', '#addElement', function () {
// createElement();
// });
$(document).on('click', '#removeAllElements', function () {
$('.element, .element-list-item').remove();
updateZIndexes();
if (confirm(l.js_common_are_you_sure)) {
$('.element, .element-list-item').remove();
updateZIndexes();
}
});
$(document).on('dblclick', '.element', function (e) {
$('.content-explr-picker[data-id='+$(this).attr('data-id')+']').click();
});
$(document).on('mousedown', function (e) {
@ -220,7 +240,7 @@ jQuery(document).ready(function ($) {
if (!keepFocusedElement) {
unfocusElements();
}
});
})
$(document).on('click', '#presetGrid2x2', function () {
let screenWidth = $('#screen').width();
@ -252,6 +272,22 @@ jQuery(document).ready(function ($) {
});
});
$(document).on('click', '.content-explr-picker', function () {
const elementId = $(this).attr('data-id');
const isNew = !elementId;
const $element = isNew ? $(createElement()) : $('#element-'+elementId);
showPickers('modal-content-explr-picker', function (content) {
console.log(content);
$element.attr('data-content-id', content.id);
$element.attr('data-content-name', content.name);
$element.attr('data-content-type', content.type);
console.log($element)
$element.find('i').get(0).classList = ['fa', content.classIcon, content.classColor].join(' ');
});
});
function updateZIndexes() {
const zindex = $('.element-list-item').length + 1;
$('.element-list-item').each(function(index) {
@ -266,6 +302,5 @@ jQuery(document).ready(function ($) {
}
});
createElement();
updateForm(null);
$('#presetGrid2x2').click();
});

View File

@ -155,8 +155,6 @@ form {
color: $gscale5;
background: none;
box-shadow: none;
border: none;
border-bottom: 1px solid $gscale3;
border-radius: 0;
}
@ -167,11 +165,8 @@ form {
&.disabled,
&[disabled] {
border: none;
background: $gscale0;
border-radius: $baseRadius;
padding-left: 10px;
padding-right: 10px;
}
}
}

View File

@ -45,14 +45,18 @@
.element {
position: absolute !important;
background-color: #f0f0f0;
outline: 1px solid rgba($black, .5);
background-color: $gkscaleE;
outline: 1px solid $gkscaleC;
text-align: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&.focused {
border: none;
outline: 2px solid blue;
outline: 2px solid $seaBlue;
z-index: 89 !important;
.ui-resizable-handle {
@ -60,6 +64,14 @@
}
}
i {
font-size: 20px;
color: $gkscaleC;
&.fa-cog {
text-shadow: 0 -2px $gkscaleB, 0 0px 2px $gkscaleB;
}
}
.rotate-handle {
width: 10px;
height: 10px;
@ -72,8 +84,8 @@
}
.ui-resizable-handle {
background: black;
border: 1px solid #000;
background: $gkscaleA;
border: 1px solid $gkscale5;
width: 10px;
height: 10px;
z-index: 90;
@ -145,25 +157,61 @@
.form-element-properties {
margin-left: 20px;
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #ccc;
flex: 1;
align-self: stretch;
form {
display: flex;
flex-direction: column;
label,
input {
margin-bottom: 10px;
}
}
#elementList {
h3 {
margin: 0 0 10px 0;
font-size: 16px;
font-weight: 500;
color: $gscaleD;
text-decoration: none;
border-bottom: 1px solid $gscale2;
margin-bottom: 20px;
padding-bottom: 10px;
align-self: stretch;
}
.form-group {
flex-direction: row;
justify-content: flex-start;
align-items: center;
display: flex;
label {
flex-grow: 1;
flex-direction: row;
justify-content: flex-start;
align-items: center;
display: flex;
font-weight: bold;
margin-right: 10px;
}
.widget {
flex-grow: 1;
flex-direction: row;
justify-content: flex-start;
align-items: center;
display: flex;
margin: 0;
input {
flex: 1;
margin: 0;
&[disabled] {
padding: 8px 0 5px 8px;
border: 1px solid rgba(255,255,255,.05);
}
}
}
}
}
}

View File

@ -110,7 +110,23 @@ When you run the browser yourself, don't forget to use these flags for chromium
```bash
# chromium or chromium-browser or even chrome
# replace http://localhost:5000 with your obscreen-studio instance url
chromium --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --noerrdialogs --kiosk --incognito --window-position=0,0 --window-size=1920,1080 --display=:0 http://localhost:5000
chromium \
--disk-cache-size=2147483648 \
--disable-features=Translate \
--ignore-certificate-errors \
--disable-web-security \
--disable-restore-session-state \
--autoplay-policy=no-user-gesture-required \
--start-maximized \
--allow-running-insecure-content \
--remember-cert-error-decisions \
--noerrdialogs \
--kiosk \
--incognito \
--window-position=0,0 \
--window-size=1920,1080 \
--display=:0 \
http://localhost:5000
```
---

View File

@ -118,7 +118,23 @@ When you run the browser yourself, don't forget to use these flags for chromium
```bash
# chromium or chromium-browser or even chrome
# replace http://localhost:5000 with your obscreen-studio instance url
chromium --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --noerrdialogs --kiosk --incognito --window-position=0,0 --window-size=1920,1080 --display=:0 http://localhost:5000
chromium \
--disk-cache-size=2147483648 \
--disable-features=Translate \
--ignore-certificate-errors \
--disable-web-security \
--disable-restore-session-state \
--autoplay-policy=no-user-gesture-required \
--start-maximized \
--allow-running-insecure-content \
--remember-cert-error-decisions \
--noerrdialogs \
--kiosk \
--incognito \
--window-position=0,0 \
--window-size=1920,1080 \
--display=:0 \
http://localhost:5000
```
---

View File

@ -110,11 +110,14 @@ class ContentController(ObController):
if not content:
return abort(404)
vargs = {}
working_folder_path, working_folder = self.get_folder_context()
edit_view = 'slideshow/contents/edit.jinja.html'
if content.type == ContentType.COMPOSITION:
edit_view = 'slideshow/contents/edit-composition.jinja.html'
vargs['folders_tree'] = self._model_store.folder().get_folder_tree(FolderEntity.CONTENT)
vargs['foldered_contents'] = self._model_store.content().get_all_indexed('folder_id', multiple=True)
return render_template(
edit_view,
@ -122,7 +125,8 @@ class ContentController(ObController):
working_folder_path=working_folder_path,
working_folder=working_folder,
enum_content_type=ContentType,
external_storage_mountpoint=self._model_store.config().map().get('external_storage_mountpoint')
external_storage_mountpoint=self._model_store.config().map().get('external_storage_mountpoint'),
**vargs
)
def slideshow_content_save(self, content_id: int = 0):

View File

@ -156,7 +156,7 @@ class PlayerController(ObController):
slide = dict(slide)
slide['id'] = hashlib.md5(str(file).encode('utf-8')).hexdigest()
slide['position'] = position
slide['delegate_duration'] = 1 if slide['type'] == ContentType.VIDEO.value else 0
slide['delegate_duration'] = 1 if virtual_content.type == ContentType.VIDEO else 0
slide['name'] = file.name
slide['type'] = virtual_content.type.value
slide['location'] = self._model_store.content().resolve_content_location(virtual_content)

View File

@ -28,7 +28,7 @@ class ContentManager(ModelManager):
"name CHAR(255)",
"type CHAR(30)",
"location TEXT",
"duration INTEGER",
"duration FLOAT",
"folder_id INTEGER",
"created_by CHAR(255)",
"updated_by CHAR(255)",

View File

@ -3,6 +3,7 @@ import os
from typing import Dict, Optional, List, Tuple, Union
from src.model.entity.Playlist import Playlist
from src.model.enum.ContentType import ContentType
from src.util.utils import get_optional_string, get_yt_video_id, slugify, slugify_next
from src.manager.DatabaseManager import DatabaseManager
from src.manager.SlideManager import SlideManager
@ -69,15 +70,17 @@ class PlaylistManager(ModelManager):
durations = self._db.execute_read_query("""
SELECT
playlist_id,
SUM(CASE
ROUND(SUM(CASE
WHEN s.delegate_duration = 1 THEN c.duration
WHEN c.type = '{}' THEN s.duration
ELSE s.duration
END) AS total_duration
END)) AS total_duration
FROM {} s
LEFT JOIN {} c ON c.id = s.content_id
WHERE cron_schedule IS NULL {}
WHERE cron_schedule IS NULL {} AND s.enabled is TRUE
GROUP BY playlist_id;
""".format(
ContentType.EXTERNAL_STORAGE.value,
SlideManager.TABLE_NAME,
ContentManager.TABLE_NAME,
"{}".format(

View File

@ -9,7 +9,7 @@ from src.util.utils import str_to_enum
class Content:
def __init__(self, uuid: str = '', location: str = '', type: Union[ContentType, str] = ContentType.URL, name: str = 'Untitled', id: Optional[int] = None, duration: Optional[int] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None, folder_id: Optional[int] = None):
def __init__(self, uuid: str = '', location: str = '', type: Union[ContentType, str] = ContentType.URL, name: str = 'Untitled', id: Optional[int] = None, duration: Optional[float] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None, folder_id: Optional[int] = None):
self._uuid = uuid if uuid else self.generate_and_set_uuid()
self._id = id if id else None
self._location = location
@ -88,11 +88,11 @@ class Content:
self._folder_id = value
@property
def duration(self) -> Optional[int]:
def duration(self) -> Optional[float]:
return self._duration
@duration.setter
def duration(self, value: Optional[int]):
def duration(self, value: Optional[float]):
self._duration = value
@property

View File

@ -10,9 +10,9 @@ def mp4_duration_with_ffprobe(filename):
fields = json.loads(result)['streams'][0]
if 'tags' in fields and 'DURATION' in fields['tags']:
return int(float(fields['tags']['DURATION']))
return round(float(fields['tags']['DURATION']), 2)
if 'duration' in fields:
return int(float(fields['duration']))
return round(float(fields['duration']), 2)
return 0

View File

@ -251,6 +251,7 @@ def slugify(value):
def seconds_to_hhmmss(seconds):
seconds = int(seconds)
if not seconds:
return ""
hours = seconds // 3600

View File

@ -16,4 +16,20 @@ WIDTH=$(echo $RESOLUTION | cut -d 'x' -f 1)
HEIGHT=$(echo $RESOLUTION | cut -d 'x' -f 2)
# Start Chromium in kiosk mode
chromium-browser --disk-cache-size=2147483648 --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --noerrdialogs --kiosk --incognito --window-position=0,0 --window-size=${WIDTH},${HEIGHT} --display=:0 http://localhost:5000
chromium-browser \
--disk-cache-size=2147483648 \
--disable-features=Translate \
--ignore-certificate-errors \
--disable-web-security \
--disable-restore-session-state \
--autoplay-policy=no-user-gesture-required \
--start-maximized \
--allow-running-insecure-content \
--remember-cert-error-decisions \
--noerrdialogs \
--kiosk \
--incognito \
--window-position=0,0 \
--window-size=${WIDTH},${HEIGHT} \
--display=:0 \
http://localhost:5000

View File

@ -84,6 +84,7 @@ systemctl set-default graphical.target
mkdir -p "$WORKING_DIR/obscreen/var/run"
curl https://raw.githubusercontent.com/jr-k/obscreen/master/system/autostart-browser-x11.sh | sed "s#/home/pi#$WORKING_DIR#g" | sed "s#=pi#=$OWNER#g" | sed "s#http://localhost:5000#$obscreen_studio_url#g" | tee "$WORKING_DIR/obscreen/var/run/play"
chmod +x "$WORKING_DIR/obscreen/var/run/play"
chown -R $OWNER:$OWNER "$WORKING_DIR/obscreen"
# ============================================================
# Start

View File

@ -25,7 +25,6 @@ apt-get install -y git python3-pip python3-venv libsqlite3-dev ntfs-3g ffmpeg
cd $WORKING_DIR
git clone https://github.com/jr-k/obscreen.git
cd obscreen
chown -R $USER:$USER ./
# Install application dependencies
python3 -m venv venv
@ -38,6 +37,9 @@ cp .env.dist .env
# Add user to needed group
usermod -aG plugdev $OWNER
# Fix permissions
chown -R $OWNER:$OWNER ./
# ============================================================
# Automount script for external storage
# ============================================================

View File

@ -2,11 +2,11 @@
<h2>
{{ l.common_pick_element }}
</h2>
{% with use_href=False %}
{% include 'fleet/node-players/component/explr-sidebar.jinja.html' %}
{% endwith %}
<div class="actions">
<button type="button" class="btn btn-naked picker-close">
<i class="fa fa-close icon-left"></i>{{ l.common_close }}

View File

@ -14,6 +14,7 @@
.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, .slide video { height: 100%; }
.slide video { width: 100%; height: 100%; }
</style>
<script type="application/javascript" src="{{ STATIC_PREFIX }}js/utils.js"></script>
<script type="application/javascript" src="{{ STATIC_PREFIX }}js/lib/is-cron-now.js"></script>
@ -82,6 +83,7 @@
let curSlide = secondSlide;
let nextSlide = firstSlide;
let notificationItemIndex = null;
let pausableContent = null;
// Functions
const itemsLoadedProcess = function() {
@ -126,6 +128,10 @@
const resume = function() {
playState = PLAY_STATE_PLAYING;
if (pausableContent) {
pausableContent.play();
}
};
const play = function() {
@ -135,6 +141,10 @@
const pause = function() {
pauseClockValue = clockValue;
playState = PLAY_STATE_PAUSE;
if (pausableContent) {
pausableContent.pause();
}
};
const stop = function() {
@ -203,7 +213,7 @@
let duration = item.duration;
if (durationsOverride[item.id]) {
if (durationsOverride[item.id] !== undefined) {
duration = durationsOverride[item.id];
}
@ -250,17 +260,15 @@
if (i === curItemIndex) {
secondsBeforeNext = accumulatedTime + safe_duration(item) - timeInCurrentLoop;
//console.log("remaining:", secondsBeforeNext, "clock:",clockValue, curItemIndex);
//console.log("id", item.id, "secondsBeforeNext:", secondsBeforeNext, "clock:", clockValue, "clockLoopDration", timeInCurrentLoop, "<", accumulatedTime , '+', safe_duration(item));
}
if (timeInCurrentLoop < accumulatedTime + safe_duration(item)) {
if (curItemIndex !== i) {
//console.log('change to ', i , item.name)
curItemIndex = i;
const emptySlide = getEmptySlide();
if ((emptySlide && !hasMoveOnce) || forcePreload) {
//console.log('init preload');
if (!hasMoveOnce && syncWithTime) {
if (accumulatedTime + safe_duration(item) - timeInCurrentLoop < 1) {
// Prevent glitch when syncWithTime for first init
@ -386,10 +394,13 @@
delayNoisyContentJIT = lookupCurrentItem().id !== item.id ? delayNoisyContentJIT : 0;
video.addEventListener('loadedmetadata', function() {
if (item.duration !== video.duration && item.delegate_duration) {
durationsOverride[item.id] = video.duration;
if (item.duration !== video.duration && !item.delegate_duration) {
console.warn('Given duration ' + item.duration + 's is different from video file ' + Math.ceil(video.duration) + 's');
}
if (item.delegate_duration) {
durationsOverride[item.id] = Math.ceil(video.duration);
}
});
const autoplayLoader = function() {
@ -400,12 +411,14 @@
if (element.innerHTML.match('<video>')) {
if (!previewMode) {
setTimeout(function() {
video.play();
video.play();
pausableContent = video;
}, 1000);
}
}
}
setTimeout(autoplayLoader, delayNoisyContentJIT);
setTimeout(autoplayLoader, delayNoisyContentJIT);
}
const checkAndMoveNotifications = function() {

View File

@ -14,10 +14,10 @@
{{ render_folder(child) }}
{% endfor %}
{% for content in content_children %}
{% set slides = slides_with_content[content.id]|default([]) %}
{% set slides = slides_with_content[content.id]|default([]) if slides_with_content else [] %}
{% set icon = enum_content_type.get_fa_icon(content.type) %}
{% set color = enum_content_type.get_color_icon(content.type) %}
<li class="explr-item" data-entity-json="{{ content.to_json() }}">
<li class="explr-item" data-entity-json="{{ content.to_json({'classIcon': icon, 'classColor': color}) }}">
<i class="fa {{ icon }} {{ color }}"></i>
{% if slides|length > 0 %}
<sub>

View File

@ -6,8 +6,8 @@
{% endblock %}
{% block add_css %}
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/flatpickr.min.css"/>
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/jquery-explr-1.4.css"/>
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/flatpickr.min.css"/>
{{ HOOK(H_SLIDESHOW_CONTENT_CSS) }}
{% endblock %}
@ -17,6 +17,7 @@
<script src="{{ STATIC_PREFIX }}js/lib/jquery-ui.min.js"></script>
{# <script src="{{ STATIC_PREFIX }} js/lib/jquery-ui-rotatable.min.js"></script> #}
<script src="{{ STATIC_PREFIX }}js/slideshow/content-composition.js"></script>
<script src="{{ STATIC_PREFIX }}js/explorer.js"></script>
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
{% endblock %}
@ -109,7 +110,7 @@
<div class="inner">
<div class="toolbar">
<button id="presetGrid2x2">Grid 2x2</button>
<button id="addElement">Add Element</button>
<button id="addElement" class="content-explr-picker">Add Element</button>
<button id="removeAllElements">Remove All Elements</button>
</div>
@ -123,23 +124,55 @@
<div class="page-panel right-panel">
<div class="form-element-properties">
<div class="form-element-properties hidden">
<form id="elementForm">
<h3>Element Properties</h3>
<label for="elem-x">X:</label>
<input type="number" id="elem-x" name="elem-x"><br>
<label for="elem-y">Y:</label>
<input type="number" id="elem-y" name="elem-y"><br>
<label for="elem-width">Width:</label>
<input type="number" id="elem-width" name="elem-width"><br>
<label for="elem-height">Height:</label>
<input type="number" id="elem-height" name="elem-height"><br>
<!--<label for="elem-rotate">Rotate (deg):</label>-->
<!--<input type="number" id="elem-rotate" name="elem-rotate"><br>-->
<div class="form-group">
<label for="elem-x">Position X</label>
<div class="widget">
<input type="number" id="elem-x" name="elem-x">
</div>
</div>
<div class="form-group">
<label for="elem-y">Position Y</label>
<div class="widget">
<input type="number" id="elem-y" name="elem-y">
</div>
</div>
<div class="form-group">
<label for="elem-width">Width</label>
<div class="widget">
<input type="number" id="elem-width" name="elem-width">
</div>
</div>
<div class="form-group">
<label for="elem-height">Height</label>
<div class="widget">
<input type="number" id="elem-height" name="elem-height">
</div>
</div>
{# <div class="form-group">#}
{# <label for="elem-rotate">Rotate (deg)</label>#}
{# <div class="widget">#}
{# <input type="number" id="elem-rotate" name="elem-rotate">#}
{# </div>#}
{# </div>#}
</form>
</div>
</div>
</div>
<div class="pickers hidden">
<div class="modals-outer">
<div class="modals-inner">
{% include 'slideshow/contents/modal/explr-picker.jinja.html' %}
</div>
</div>
</div>
{% endblock %}