Stable: Fullscreen player, intro video, playlist update, overlay controls, clean shutdown, mouse hide, and robust exit fixes
This commit is contained in:
Binary file not shown.
BIN
signage_player/__pycache__/player.cpython-311.pyc
Normal file
BIN
signage_player/__pycache__/player.cpython-311.pyc
Normal file
Binary file not shown.
@@ -105,6 +105,38 @@ def download_media_files(playlist, media_dir):
|
|||||||
updated_playlist.append(updated_media)
|
updated_playlist.append(updated_media)
|
||||||
return updated_playlist
|
return updated_playlist
|
||||||
|
|
||||||
|
def delete_old_playlists_and_media(current_version, playlist_dir, media_dir, keep_versions=1):
|
||||||
|
"""
|
||||||
|
Delete old playlist files and media files not referenced by the latest playlist version.
|
||||||
|
keep_versions: number of latest versions to keep (default 1)
|
||||||
|
"""
|
||||||
|
# Find all playlist files
|
||||||
|
playlist_files = [f for f in os.listdir(playlist_dir) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||||
|
# Keep only the latest N versions
|
||||||
|
versions = sorted([int(f.split('_v')[-1].split('.json')[0]) for f in playlist_files], reverse=True)
|
||||||
|
keep = set(versions[:keep_versions])
|
||||||
|
# Delete old playlist files
|
||||||
|
for f in playlist_files:
|
||||||
|
v = int(f.split('_v')[-1].split('.json')[0])
|
||||||
|
if v not in keep:
|
||||||
|
os.remove(os.path.join(playlist_dir, f))
|
||||||
|
# Collect all media files referenced by the kept playlists
|
||||||
|
referenced = set()
|
||||||
|
for v in keep:
|
||||||
|
path = os.path.join(playlist_dir, f'server_playlist_v{v}.json')
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
for item in data.get('playlist', []):
|
||||||
|
referenced.add(item.get('file_name'))
|
||||||
|
# Delete media files not referenced
|
||||||
|
for f in os.listdir(media_dir):
|
||||||
|
if f not in referenced:
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(media_dir, f))
|
||||||
|
except Exception as e:
|
||||||
|
Logger.warning(f"Failed to delete media file {f}: {e}")
|
||||||
|
|
||||||
def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_dir):
|
def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_dir):
|
||||||
"""
|
"""
|
||||||
Fetch the server playlist once, compare versions, and update if needed.
|
Fetch the server playlist once, compare versions, and update if needed.
|
||||||
@@ -125,6 +157,8 @@ def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_d
|
|||||||
updated_playlist = download_media_files(server_data['playlist'], media_dir)
|
updated_playlist = download_media_files(server_data['playlist'], media_dir)
|
||||||
server_data['playlist'] = updated_playlist
|
server_data['playlist'] = updated_playlist
|
||||||
save_playlist_with_version(server_data, playlist_dir)
|
save_playlist_with_version(server_data, playlist_dir)
|
||||||
|
# Delete old playlists and unreferenced media
|
||||||
|
delete_old_playlists_and_media(server_version, playlist_dir, media_dir)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
Logger.warning("No playlist data fetched from server or playlist is empty.")
|
Logger.warning("No playlist data fetched from server or playlist is empty.")
|
||||||
@@ -135,4 +169,3 @@ def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_d
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ MEDIA_DATA_PATH = os.path.join(os.path.dirname(__file__), 'static_data', 'media'
|
|||||||
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
||||||
LOCAL_PLAYLIST_PATH = os.path.join(PLAYLIST_DIR, 'server_playlist.json')
|
LOCAL_PLAYLIST_PATH = os.path.join(PLAYLIST_DIR, 'server_playlist.json')
|
||||||
|
|
||||||
def playlist_update_loop(refresh_time, stop_event, config, local_playlist_path, media_dir, playlist_dir):
|
def playlist_update_loop(refresh_time, app_running, config, local_playlist_path, media_dir, playlist_dir):
|
||||||
while not stop_event.is_set():
|
while app_running[0]:
|
||||||
updated = update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_dir)
|
updated = update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_dir)
|
||||||
if updated:
|
if updated:
|
||||||
print(f"[REFRESH] Playlist updated from server at {time.strftime('%X')}")
|
print(f"[REFRESH] Playlist updated from server at {time.strftime('%X')}")
|
||||||
@@ -18,24 +18,54 @@ def playlist_update_loop(refresh_time, stop_event, config, local_playlist_path,
|
|||||||
print(f"[REFRESH] Playlist already up to date at {time.strftime('%X')}")
|
print(f"[REFRESH] Playlist already up to date at {time.strftime('%X')}")
|
||||||
time.sleep(refresh_time)
|
time.sleep(refresh_time)
|
||||||
|
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from player import SimpleTkPlayer, load_latest_playlist
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
with open(CONFIG_PATH, 'r') as f:
|
with open(CONFIG_PATH, 'r') as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
refresh_time = int(config.get('refresh_time', 5)) * 60 # minutes
|
refresh_time = int(config.get('refresh_time', 5)) * 60 # minutes
|
||||||
stop_event = threading.Event()
|
app_running = [True]
|
||||||
update_thread = threading.Thread(
|
update_thread = threading.Thread(
|
||||||
target=playlist_update_loop,
|
target=playlist_update_loop,
|
||||||
args=(refresh_time, stop_event, config, LOCAL_PLAYLIST_PATH, MEDIA_DATA_PATH, PLAYLIST_DIR),
|
args=(refresh_time, app_running, config, LOCAL_PLAYLIST_PATH, MEDIA_DATA_PATH, PLAYLIST_DIR),
|
||||||
daemon=True
|
daemon=True
|
||||||
)
|
)
|
||||||
update_thread.start()
|
update_thread.start()
|
||||||
print("Playlist update thread started. Press Ctrl+C to exit.")
|
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title("Simple Tkinter Player")
|
||||||
|
root.configure(bg='black')
|
||||||
|
root.attributes('-fullscreen', True)
|
||||||
|
|
||||||
|
# Load playlist and create player
|
||||||
|
playlist = load_latest_playlist()
|
||||||
|
player = SimpleTkPlayer(root, playlist)
|
||||||
|
player.app_running = app_running
|
||||||
|
orig_exit_app = player.exit_app
|
||||||
|
def exit_and_stop():
|
||||||
|
app_running[0] = False
|
||||||
|
orig_exit_app()
|
||||||
|
player.exit_app = exit_and_stop
|
||||||
|
player.exit_btn.config(command=player.exit_app)
|
||||||
|
player.main_start()
|
||||||
|
|
||||||
|
def reload_playlist_if_updated():
|
||||||
|
new_playlist = load_latest_playlist()
|
||||||
|
if new_playlist != player.playlist:
|
||||||
|
player.playlist = new_playlist
|
||||||
|
player.current_index = 0
|
||||||
|
player.show_current_media()
|
||||||
|
root.after(10000, reload_playlist_if_updated)
|
||||||
|
reload_playlist_if_updated()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
root.mainloop()
|
||||||
time.sleep(1)
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("Exiting...")
|
pass
|
||||||
stop_event.set()
|
finally:
|
||||||
|
app_running[0] = False
|
||||||
update_thread.join()
|
update_thread.join()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"screen_orientation": "Landscape",
|
"screen_orientation": "Landscape",
|
||||||
"screen_name": "tv-terasa",
|
"screen_name": "tv-terasa",
|
||||||
"quickconnect_key": "8887779",
|
"quickconnect_key": "8887779",
|
||||||
"server_ip": "digi-signage.moto-adv.com",
|
"server_ip": "192.168.1.22",
|
||||||
"port": "8880",
|
"port": "80",
|
||||||
"screen_w": "1920",
|
"screen_w": "1920",
|
||||||
"screen_h": "1080",
|
"screen_h": "1080",
|
||||||
"refresh_time": "5"
|
"refresh_time": "5"
|
||||||
|
|||||||
BIN
signage_player/main_data/intro1.mp4
Normal file
BIN
signage_player/main_data/intro1.mp4
Normal file
Binary file not shown.
@@ -20,3 +20,176 @@
|
|||||||
[INFO] [SignageApp] Successfully downloaded trans_cindrel_4.jpg to /home/pi/Desktop/tkinter_player/signage_player/static_data/media/trans_cindrel_4.jpg
|
[INFO] [SignageApp] Successfully downloaded trans_cindrel_4.jpg to /home/pi/Desktop/tkinter_player/signage_player/static_data/media/trans_cindrel_4.jpg
|
||||||
[INFO] [SignageApp] Preparing to download 101394-video-1080.mp4 from http://digi-signage.moto-adv.com/media/101394-video-1080.mp4...
|
[INFO] [SignageApp] Preparing to download 101394-video-1080.mp4 from http://digi-signage.moto-adv.com/media/101394-video-1080.mp4...
|
||||||
[ERROR] [SignageApp] Failed to download 101394-video-1080.mp4. Status Code: 404
|
[ERROR] [SignageApp] Failed to download 101394-video-1080.mp4. Status Code: 404
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 502
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
|
||||||
|
[INFO] [SignageApp] Local playlist is already up to date.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 502
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
|
||||||
|
[INFO] [SignageApp] Local playlist is already up to date.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 502
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
|
||||||
|
[INFO] [SignageApp] Local playlist is already up to date.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 502
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
|
||||||
|
[INFO] [SignageApp] Local playlist is already up to date.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 502
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
|
||||||
|
[INFO] [SignageApp] Local playlist is already up to date.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 502
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
|
||||||
|
[INFO] [SignageApp] Local playlist is already up to date.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 502
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
|
||||||
|
[INFO] [SignageApp] Local playlist is already up to date.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 502
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist: HTTPConnectionPool(host='192.168.1.22', port=8880): Max retries exceeded with url: /api/playlists?hostname=tv-terasa&quickconnect_code=8887779 (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0xf68c5ff0>: Failed to establish a new connection: [Errno 111] Connection refused'))
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist: HTTPConnectionPool(host='192.168.1.22', port=8880): Max retries exceeded with url: /api/playlists?hostname=tv-terasa&quickconnect_code=8887779 (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0xf72d3550>: Failed to establish a new connection: [Errno 111] Connection refused'))
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$YL2Id5c4CT5yE31KlK0ZF.mK033OimRNKbMR2ehTpGLFyPmDxnEOy', 'playlist': [{'duration': 20, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}], 'playlist_version': 2}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$YL2Id5c4CT5yE31KlK0ZF.mK033OimRNKbMR2ehTpGLFyPmDxnEOy', 'playlist': [{'duration': 20, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}], 'playlist_version': 2}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 2
|
||||||
|
[INFO] [SignageApp] Preparing to download call-of-duty-black-3840x2160-23674.jpg from http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg...
|
||||||
|
[INFO] [SignageApp] Successfully downloaded call-of-duty-black-3840x2160-23674.jpg to /home/pi/Desktop/tkinter_player/signage_player/static_data/media/call-of-duty-black-3840x2160-23674.jpg
|
||||||
|
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4...
|
||||||
|
[ERROR] [SignageApp] Failed to download big-buck-bunny-1080p-60fps-30sec.mp4. Status Code: 404
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$YL2Id5c4CT5yE31KlK0ZF.mK033OimRNKbMR2ehTpGLFyPmDxnEOy', 'playlist': [{'duration': 20, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}], 'playlist_version': 2}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 2
|
||||||
|
[INFO] [SignageApp] Preparing to download call-of-duty-black-3840x2160-23674.jpg from http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg...
|
||||||
|
[INFO] [SignageApp] File call-of-duty-black-3840x2160-23674.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4...
|
||||||
|
[ERROR] [SignageApp] Failed to download big-buck-bunny-1080p-60fps-30sec.mp4. Status Code: 404
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$YL2Id5c4CT5yE31KlK0ZF.mK033OimRNKbMR2ehTpGLFyPmDxnEOy', 'playlist': [{'duration': 20, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}], 'playlist_version': 2}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 2
|
||||||
|
[INFO] [SignageApp] Preparing to download call-of-duty-black-3840x2160-23674.jpg from http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg...
|
||||||
|
[INFO] [SignageApp] Successfully downloaded call-of-duty-black-3840x2160-23674.jpg to /home/pi/Desktop/tkinter_player/signage_player/static_data/media/call-of-duty-black-3840x2160-23674.jpg
|
||||||
|
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4...
|
||||||
|
[ERROR] [SignageApp] Failed to download big-buck-bunny-1080p-60fps-30sec.mp4. Status Code: 404
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$YL2Id5c4CT5yE31KlK0ZF.mK033OimRNKbMR2ehTpGLFyPmDxnEOy', 'playlist': [{'duration': 20, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}], 'playlist_version': 2}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 2
|
||||||
|
[INFO] [SignageApp] Preparing to download call-of-duty-black-3840x2160-23674.jpg from http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg...
|
||||||
|
[INFO] [SignageApp] File call-of-duty-black-3840x2160-23674.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4...
|
||||||
|
[ERROR] [SignageApp] Failed to download big-buck-bunny-1080p-60fps-30sec.mp4. Status Code: 404
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$YL2Id5c4CT5yE31KlK0ZF.mK033OimRNKbMR2ehTpGLFyPmDxnEOy', 'playlist': [{'duration': 20, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}, {'duration': 10, 'file_name': 'one-piece-season-2-5120x2880-23673.jpg', 'url': 'http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}], 'playlist_version': 3}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 3
|
||||||
|
[INFO] [SignageApp] Preparing to download call-of-duty-black-3840x2160-23674.jpg from http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg...
|
||||||
|
[INFO] [SignageApp] File call-of-duty-black-3840x2160-23674.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download one-piece-season-2-5120x2880-23673.jpg from http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg...
|
||||||
|
[INFO] [SignageApp] Successfully downloaded one-piece-season-2-5120x2880-23673.jpg to /home/pi/Desktop/tkinter_player/signage_player/static_data/media/one-piece-season-2-5120x2880-23673.jpg
|
||||||
|
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4...
|
||||||
|
[ERROR] [SignageApp] Failed to download big-buck-bunny-1080p-60fps-30sec.mp4. Status Code: 404
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist: HTTPConnectionPool(host='192.168.1.22', port=80): Max retries exceeded with url: /api/playlists?hostname=tv-terasa&quickconnect_code=8887779 (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0xf5c3b710>: Failed to establish a new connection: [Errno 111] Connection refused'))
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
|
||||||
|
[INFO] [SignageApp] Local playlist is already up to date.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist: HTTPConnectionPool(host='192.168.1.22', port=80): Max retries exceeded with url: /api/playlists?hostname=tv-terasa&quickconnect_code=8887779 (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0xf58c07b0>: Failed to establish a new connection: [Errno 111] Connection refused'))
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
|
||||||
|
[INFO] [SignageApp] Local playlist is already up to date.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist: HTTPConnectionPool(host='192.168.1.22', port=80): Max retries exceeded with url: /api/playlists?hostname=tv-terasa&quickconnect_code=8887779 (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0xf58e6930>: Failed to establish a new connection: [Errno 111] Connection refused'))
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
|
||||||
|
[INFO] [SignageApp] Local playlist is already up to date.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist: HTTPConnectionPool(host='192.168.1.22', port=80): Max retries exceeded with url: /api/playlists?hostname=tv-terasa&quickconnect_code=8887779 (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0xf5726810>: Failed to establish a new connection: [Errno 111] Connection refused'))
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
|
||||||
|
[INFO] [SignageApp] Local playlist is already up to date.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$YL2Id5c4CT5yE31KlK0ZF.mK033OimRNKbMR2ehTpGLFyPmDxnEOy', 'playlist': [{'duration': 20, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}, {'duration': 10, 'file_name': 'one-piece-season-2-5120x2880-23673.jpg', 'url': 'http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}], 'playlist_version': 3}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 3
|
||||||
|
[INFO] [SignageApp] Preparing to download call-of-duty-black-3840x2160-23674.jpg from http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg...
|
||||||
|
[INFO] [SignageApp] File call-of-duty-black-3840x2160-23674.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download one-piece-season-2-5120x2880-23673.jpg from http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg...
|
||||||
|
[INFO] [SignageApp] File one-piece-season-2-5120x2880-23673.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4...
|
||||||
|
[ERROR] [SignageApp] Failed to download big-buck-bunny-1080p-60fps-30sec.mp4. Status Code: 404
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$YL2Id5c4CT5yE31KlK0ZF.mK033OimRNKbMR2ehTpGLFyPmDxnEOy', 'playlist': [{'duration': 20, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}, {'duration': 10, 'file_name': 'one-piece-season-2-5120x2880-23673.jpg', 'url': 'http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}], 'playlist_version': 3}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 3
|
||||||
|
[INFO] [SignageApp] Preparing to download call-of-duty-black-3840x2160-23674.jpg from http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg...
|
||||||
|
[INFO] [SignageApp] File call-of-duty-black-3840x2160-23674.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download one-piece-season-2-5120x2880-23673.jpg from http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg...
|
||||||
|
[INFO] [SignageApp] File one-piece-season-2-5120x2880-23673.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4...
|
||||||
|
[ERROR] [SignageApp] Failed to download big-buck-bunny-1080p-60fps-30sec.mp4. Status Code: 404
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 404
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
|
||||||
|
[INFO] [SignageApp] Local playlist is already up to date.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$4dxmxuyiezoojRSThjmzQeVsaBscU5vP.9GcTPJhXymmL9JsNklea', 'playlist': [{'duration': 10, 'file_name': 'one-piece-season-2-5120x2880-23673.jpg', 'url': 'http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}, {'duration': 10, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}], 'playlist_version': 2}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 2
|
||||||
|
[INFO] [SignageApp] Preparing to download one-piece-season-2-5120x2880-23673.jpg from http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg...
|
||||||
|
[INFO] [SignageApp] File one-piece-season-2-5120x2880-23673.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4...
|
||||||
|
[INFO] [SignageApp] Successfully downloaded big-buck-bunny-1080p-60fps-30sec.mp4 to /home/pi/Desktop/tkinter_player/signage_player/static_data/media/big-buck-bunny-1080p-60fps-30sec.mp4
|
||||||
|
[INFO] [SignageApp] Preparing to download call-of-duty-black-3840x2160-23674.jpg from http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg...
|
||||||
|
[INFO] [SignageApp] File call-of-duty-black-3840x2160-23674.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$4dxmxuyiezoojRSThjmzQeVsaBscU5vP.9GcTPJhXymmL9JsNklea', 'playlist': [{'duration': 10, 'file_name': 'one-piece-season-2-5120x2880-23673.jpg', 'url': 'http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}, {'duration': 10, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}], 'playlist_version': 2}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 2
|
||||||
|
[INFO] [SignageApp] Preparing to download one-piece-season-2-5120x2880-23673.jpg from http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg...
|
||||||
|
[INFO] [SignageApp] File one-piece-season-2-5120x2880-23673.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4...
|
||||||
|
[INFO] [SignageApp] Successfully downloaded big-buck-bunny-1080p-60fps-30sec.mp4 to /home/pi/Desktop/tkinter_player/signage_player/static_data/media/big-buck-bunny-1080p-60fps-30sec.mp4
|
||||||
|
[INFO] [SignageApp] Preparing to download call-of-duty-black-3840x2160-23674.jpg from http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg...
|
||||||
|
[INFO] [SignageApp] File call-of-duty-black-3840x2160-23674.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$4dxmxuyiezoojRSThjmzQeVsaBscU5vP.9GcTPJhXymmL9JsNklea', 'playlist': [{'duration': 10, 'file_name': 'one-piece-season-2-5120x2880-23673.jpg', 'url': 'http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}, {'duration': 10, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}, {'duration': 10, 'file_name': 'demo2.jpeg', 'url': 'http://192.168.1.22/media/demo2.jpeg'}], 'playlist_version': 3}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 3
|
||||||
|
[INFO] [SignageApp] Preparing to download one-piece-season-2-5120x2880-23673.jpg from http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg...
|
||||||
|
[INFO] [SignageApp] File one-piece-season-2-5120x2880-23673.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4...
|
||||||
|
[INFO] [SignageApp] Successfully downloaded big-buck-bunny-1080p-60fps-30sec.mp4 to /home/pi/Desktop/tkinter_player/signage_player/static_data/media/big-buck-bunny-1080p-60fps-30sec.mp4
|
||||||
|
[INFO] [SignageApp] Preparing to download call-of-duty-black-3840x2160-23674.jpg from http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg...
|
||||||
|
[INFO] [SignageApp] File call-of-duty-black-3840x2160-23674.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download demo2.jpeg from http://192.168.1.22/media/demo2.jpeg...
|
||||||
|
[INFO] [SignageApp] Successfully downloaded demo2.jpeg to /home/pi/Desktop/tkinter_player/signage_player/static_data/media/demo2.jpeg
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$4dxmxuyiezoojRSThjmzQeVsaBscU5vP.9GcTPJhXymmL9JsNklea', 'playlist': [{'duration': 10, 'file_name': 'one-piece-season-2-5120x2880-23673.jpg', 'url': 'http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}, {'duration': 10, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}, {'duration': 10, 'file_name': 'demo2.jpeg', 'url': 'http://192.168.1.22/media/demo2.jpeg'}], 'playlist_version': 3}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 3
|
||||||
|
[INFO] [SignageApp] Preparing to download one-piece-season-2-5120x2880-23673.jpg from http://192.168.1.22/media/one-piece-season-2-5120x2880-23673.jpg...
|
||||||
|
[INFO] [SignageApp] File one-piece-season-2-5120x2880-23673.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4...
|
||||||
|
[INFO] [SignageApp] File big-buck-bunny-1080p-60fps-30sec.mp4 already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download call-of-duty-black-3840x2160-23674.jpg from http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg...
|
||||||
|
[INFO] [SignageApp] File call-of-duty-black-3840x2160-23674.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download demo2.jpeg from http://192.168.1.22/media/demo2.jpeg...
|
||||||
|
[INFO] [SignageApp] File demo2.jpeg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Fetching playlist from URL: http://192.168.1.22:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
|
||||||
|
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$4dxmxuyiezoojRSThjmzQeVsaBscU5vP.9GcTPJhXymmL9JsNklea', 'playlist': [{'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4'}, {'duration': 10, 'file_name': 'call-of-duty-black-3840x2160-23674.jpg', 'url': 'http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg'}, {'duration': 10, 'file_name': 'demo2.jpeg', 'url': 'http://192.168.1.22/media/demo2.jpeg'}], 'playlist_version': 4}
|
||||||
|
[INFO] [SignageApp] Fetched updated playlist from server.
|
||||||
|
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 4
|
||||||
|
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://192.168.1.22/media/big-buck-bunny-1080p-60fps-30sec.mp4...
|
||||||
|
[INFO] [SignageApp] File big-buck-bunny-1080p-60fps-30sec.mp4 already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download call-of-duty-black-3840x2160-23674.jpg from http://192.168.1.22/media/call-of-duty-black-3840x2160-23674.jpg...
|
||||||
|
[INFO] [SignageApp] File call-of-duty-black-3840x2160-23674.jpg already exists. Skipping download.
|
||||||
|
[INFO] [SignageApp] Preparing to download demo2.jpeg from http://192.168.1.22/media/demo2.jpeg...
|
||||||
|
[INFO] [SignageApp] File demo2.jpeg already exists. Skipping download.
|
||||||
|
|||||||
224
signage_player/player copy.py
Normal file
224
signage_player/player copy.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import tkinter as tk
|
||||||
|
from PIL import Image, ImageTk
|
||||||
|
import vlc
|
||||||
|
|
||||||
|
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
|
||||||
|
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
||||||
|
MEDIA_DATA_PATH = os.path.join(os.path.dirname(__file__), 'static_data', 'media')
|
||||||
|
|
||||||
|
class SimpleTkPlayer:
|
||||||
|
def __init__(self, root, playlist):
|
||||||
|
self.root = root
|
||||||
|
self.playlist = playlist
|
||||||
|
self.current_index = 0
|
||||||
|
self.paused = False
|
||||||
|
self.pause_timer = None
|
||||||
|
self.label = tk.Label(root, bg='black')
|
||||||
|
self.label.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.create_controls()
|
||||||
|
self.hide_controls()
|
||||||
|
self.root.bind('<Motion>', self.on_activity)
|
||||||
|
self.root.bind('<Button-1>', self.on_activity)
|
||||||
|
self.root.after(100, self.ensure_fullscreen)
|
||||||
|
|
||||||
|
def ensure_fullscreen(self):
|
||||||
|
self.root.attributes('-fullscreen', True)
|
||||||
|
self.root.update_idletasks()
|
||||||
|
|
||||||
|
def create_controls(self):
|
||||||
|
self.controls_frame = tk.Frame(self.root, bg='#222', bd=2, relief='ridge')
|
||||||
|
self.controls_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||||||
|
btn_style = {
|
||||||
|
'bg': '#333',
|
||||||
|
'fg': 'white',
|
||||||
|
'activebackground': '#555',
|
||||||
|
'activeforeground': '#00e6e6',
|
||||||
|
'font': ('Arial', 16, 'bold'),
|
||||||
|
'bd': 0,
|
||||||
|
'highlightthickness': 0,
|
||||||
|
'relief': 'flat',
|
||||||
|
'cursor': 'hand2',
|
||||||
|
'padx': 10,
|
||||||
|
'pady': 6
|
||||||
|
}
|
||||||
|
self.prev_btn = tk.Button(self.controls_frame, text='⏮ Prev', command=self.prev_media, **btn_style)
|
||||||
|
self.prev_btn.grid(row=0, column=0, padx=4)
|
||||||
|
self.pause_btn = tk.Button(self.controls_frame, text='⏸ Pause', command=self.toggle_pause, **btn_style)
|
||||||
|
self.pause_btn.grid(row=0, column=1, padx=4)
|
||||||
|
self.next_btn = tk.Button(self.controls_frame, text='Next ⏭', command=self.next_media, **btn_style)
|
||||||
|
self.next_btn.grid(row=0, column=2, padx=4)
|
||||||
|
self.settings_btn = tk.Button(self.controls_frame, text='⚙ Settings', command=self.open_settings, **btn_style)
|
||||||
|
self.settings_btn.grid(row=0, column=3, padx=4)
|
||||||
|
self.exit_btn = tk.Button(self.controls_frame, text='⏻ Exit', command=self.exit_app, **btn_style)
|
||||||
|
self.exit_btn.grid(row=0, column=4, padx=4)
|
||||||
|
self.exit_btn.config(fg='#ff4d4d')
|
||||||
|
|
||||||
|
def show_controls(self):
|
||||||
|
self.controls_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||||||
|
self.controls_frame.lift()
|
||||||
|
self.schedule_hide_controls()
|
||||||
|
|
||||||
|
def hide_controls(self):
|
||||||
|
self.controls_frame.place_forget()
|
||||||
|
|
||||||
|
def schedule_hide_controls(self):
|
||||||
|
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
|
||||||
|
self.root.after_cancel(self.hide_controls_timer)
|
||||||
|
self.hide_controls_timer = self.root.after(5000, self.hide_controls)
|
||||||
|
|
||||||
|
def on_activity(self, event=None):
|
||||||
|
self.show_controls()
|
||||||
|
|
||||||
|
def prev_media(self):
|
||||||
|
self.current_index = (self.current_index - 1) % len(self.playlist)
|
||||||
|
self.show_current_media()
|
||||||
|
|
||||||
|
def next_media(self):
|
||||||
|
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||||
|
self.show_current_media()
|
||||||
|
|
||||||
|
def toggle_pause(self):
|
||||||
|
if not self.paused:
|
||||||
|
self.paused = True
|
||||||
|
self.pause_btn.config(text='▶ Resume')
|
||||||
|
self.pause_timer = self.root.after(30000, self.resume_play)
|
||||||
|
else:
|
||||||
|
self.resume_play()
|
||||||
|
|
||||||
|
def resume_play(self):
|
||||||
|
self.paused = False
|
||||||
|
self.pause_btn.config(text='⏸ Pause')
|
||||||
|
if self.pause_timer:
|
||||||
|
self.root.after_cancel(self.pause_timer)
|
||||||
|
self.pause_timer = None
|
||||||
|
|
||||||
|
def play_intro_video(self):
|
||||||
|
intro_path = os.path.join(os.path.dirname(__file__), 'main_data', 'intro1.mp4')
|
||||||
|
if os.path.exists(intro_path):
|
||||||
|
self.show_video(intro_path, on_end=self.after_intro)
|
||||||
|
else:
|
||||||
|
self.after_intro()
|
||||||
|
|
||||||
|
def after_intro(self):
|
||||||
|
self.show_current_media()
|
||||||
|
self.root.after(100, self.next_media_loop)
|
||||||
|
|
||||||
|
def show_video(self, file_path, on_end=None):
|
||||||
|
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||||
|
self.vlc_player.stop()
|
||||||
|
if not hasattr(self, 'video_canvas'):
|
||||||
|
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
|
||||||
|
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.label.pack_forget()
|
||||||
|
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.root.attributes('-fullscreen', True)
|
||||||
|
self.root.update_idletasks()
|
||||||
|
self.vlc_instance = vlc.Instance()
|
||||||
|
self.vlc_player = self.vlc_instance.media_player_new()
|
||||||
|
self.vlc_player.set_mrl(file_path)
|
||||||
|
self.vlc_player.set_fullscreen(True)
|
||||||
|
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
||||||
|
self.vlc_player.play()
|
||||||
|
def check_end():
|
||||||
|
if self.vlc_player.get_state() == vlc.State.Ended:
|
||||||
|
self.video_canvas.pack_forget()
|
||||||
|
self.label.pack(fill=tk.BOTH, expand=True)
|
||||||
|
if on_end:
|
||||||
|
on_end()
|
||||||
|
else:
|
||||||
|
self.root.after(200, check_end)
|
||||||
|
check_end()
|
||||||
|
|
||||||
|
def show_current_media(self):
|
||||||
|
self.root.attributes('-fullscreen', True)
|
||||||
|
self.root.update_idletasks()
|
||||||
|
if not self.playlist:
|
||||||
|
self.label.config(text="No media available", fg='white', font=('Arial', 32))
|
||||||
|
return
|
||||||
|
media = self.playlist[self.current_index]
|
||||||
|
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
|
||||||
|
if file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
||||||
|
self.show_video(file_path, on_end=self.next_media)
|
||||||
|
elif file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
||||||
|
try:
|
||||||
|
img = Image.open(file_path)
|
||||||
|
# Fit to screen without crop or stretch
|
||||||
|
screen_w = self.root.winfo_screenwidth()
|
||||||
|
screen_h = self.root.winfo_screenheight()
|
||||||
|
img_w, img_h = img.size
|
||||||
|
scale = min(screen_w / img_w, screen_h / img_h)
|
||||||
|
new_w = int(img_w * scale)
|
||||||
|
new_h = int(img_h * scale)
|
||||||
|
img = img.resize((new_w, new_h), Image.LANCZOS)
|
||||||
|
bg = Image.new('RGB', (screen_w, screen_h), 'black')
|
||||||
|
x = (screen_w - new_w) // 2
|
||||||
|
y = (screen_h - new_h) // 2
|
||||||
|
bg.paste(img, (x, y))
|
||||||
|
photo = ImageTk.PhotoImage(bg)
|
||||||
|
self.label.config(image=photo, text='')
|
||||||
|
self.label.image = photo
|
||||||
|
except Exception as e:
|
||||||
|
self.label.config(text=f"Image error: {e}", fg='red')
|
||||||
|
else:
|
||||||
|
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
|
||||||
|
|
||||||
|
def next_media_loop(self):
|
||||||
|
if not self.playlist or self.paused:
|
||||||
|
self.root.after(1000, self.next_media_loop)
|
||||||
|
return
|
||||||
|
duration = self.playlist[self.current_index].get('duration', 10)
|
||||||
|
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||||
|
self.show_current_media()
|
||||||
|
self.root.after(duration * 1000, self.next_media_loop)
|
||||||
|
|
||||||
|
def exit_app(self):
|
||||||
|
# Signal the update thread to stop if stop_event is present
|
||||||
|
if hasattr(self, 'stop_event') and self.stop_event:
|
||||||
|
self.stop_event.set()
|
||||||
|
self.root.destroy()
|
||||||
|
|
||||||
|
def open_settings(self):
|
||||||
|
if self.paused is not True:
|
||||||
|
self.paused = True
|
||||||
|
self.pause_btn.config(text='▶ Resume')
|
||||||
|
settings_win = tk.Toplevel(self.root)
|
||||||
|
settings_win.title('Settings')
|
||||||
|
settings_win.geometry('400x300+100+100')
|
||||||
|
settings_win.transient(self.root)
|
||||||
|
settings_win.grab_set()
|
||||||
|
tk.Label(settings_win, text='Settings', font=('Arial', 18)).pack(pady=10)
|
||||||
|
# Example setting: close button
|
||||||
|
tk.Button(settings_win, text='Close', command=settings_win.destroy).pack(pady=20)
|
||||||
|
def on_close():
|
||||||
|
settings_win.grab_release()
|
||||||
|
settings_win.destroy()
|
||||||
|
self.resume_play()
|
||||||
|
settings_win.protocol('WM_DELETE_WINDOW', on_close)
|
||||||
|
settings_win.bind('<Destroy>', lambda e: self.resume_play() if not settings_win.winfo_exists() else None)
|
||||||
|
|
||||||
|
def main_start(self):
|
||||||
|
self.play_intro_video()
|
||||||
|
|
||||||
|
def load_latest_playlist():
|
||||||
|
files = [f for f in os.listdir(PLAYLIST_DIR) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||||
|
if not files:
|
||||||
|
return []
|
||||||
|
files.sort(key=lambda x: int(x.split('_v')[-1].split('.json')[0]), reverse=True)
|
||||||
|
with open(os.path.join(PLAYLIST_DIR, files[0]), 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data.get('playlist', [])
|
||||||
|
|
||||||
|
def main():
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title("Simple Tkinter Player")
|
||||||
|
root.configure(bg='black')
|
||||||
|
root.attributes('-fullscreen', True)
|
||||||
|
playlist = load_latest_playlist()
|
||||||
|
player = SimpleTkPlayer(root, playlist)
|
||||||
|
player.main_start()
|
||||||
|
root.mainloop()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -2,6 +2,7 @@ import os
|
|||||||
import json
|
import json
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
|
import vlc
|
||||||
|
|
||||||
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
|
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
|
||||||
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
||||||
@@ -20,30 +21,86 @@ class SimpleTkPlayer:
|
|||||||
self.hide_controls()
|
self.hide_controls()
|
||||||
self.root.bind('<Motion>', self.on_activity)
|
self.root.bind('<Motion>', self.on_activity)
|
||||||
self.root.bind('<Button-1>', self.on_activity)
|
self.root.bind('<Button-1>', self.on_activity)
|
||||||
self.show_current_media()
|
self.root.after(100, self.ensure_fullscreen)
|
||||||
self.root.after(100, self.next_media_loop)
|
self.root.after(200, self.hide_mouse)
|
||||||
|
self.root.after(300, self.move_mouse_to_corner)
|
||||||
|
self.root.protocol('WM_DELETE_WINDOW', self.exit_app)
|
||||||
|
|
||||||
|
def ensure_fullscreen(self):
|
||||||
|
self.root.attributes('-fullscreen', True)
|
||||||
|
self.root.update_idletasks()
|
||||||
|
|
||||||
def create_controls(self):
|
def create_controls(self):
|
||||||
self.controls_frame = tk.Frame(self.root, bg='#222')
|
# Create a transparent, borderless top-level window for controls
|
||||||
self.controls_frame.place(relx=0.98, rely=0.98, anchor='se')
|
self.controls_win = tk.Toplevel(self.root)
|
||||||
self.prev_btn = tk.Button(self.controls_frame, text='⏮ Prev', command=self.prev_media, width=8)
|
self.controls_win.overrideredirect(True)
|
||||||
self.prev_btn.grid(row=0, column=0, padx=2)
|
self.controls_win.attributes('-topmost', True)
|
||||||
self.pause_btn = tk.Button(self.controls_frame, text='⏸ Pause', command=self.toggle_pause, width=8)
|
self.controls_win.attributes('-alpha', 0.92)
|
||||||
self.pause_btn.grid(row=0, column=1, padx=2)
|
self.controls_win.configure(bg='')
|
||||||
self.next_btn = tk.Button(self.controls_frame, text='Next ⏭', command=self.next_media, width=8)
|
# Place the window at the bottom right
|
||||||
self.next_btn.grid(row=0, column=2, padx=2)
|
def place_controls():
|
||||||
self.settings_btn = tk.Button(self.controls_frame, text='⚙ Settings', command=self.open_settings, width=10)
|
self.controls_win.update_idletasks()
|
||||||
self.settings_btn.grid(row=0, column=3, padx=2)
|
w = self.controls_win.winfo_reqwidth()
|
||||||
self.exit_btn = tk.Button(self.controls_frame, text='⏻ Exit', command=self.exit_app, width=8, fg='red')
|
h = self.controls_win.winfo_reqheight()
|
||||||
self.exit_btn.grid(row=0, column=4, padx=2)
|
sw = self.root.winfo_screenwidth()
|
||||||
|
sh = self.root.winfo_screenheight()
|
||||||
|
x = sw - w - 30
|
||||||
|
y = sh - h - 30
|
||||||
|
self.controls_win.geometry(f'+{x}+{y}')
|
||||||
|
self.controls_frame = tk.Frame(self.controls_win, bg='#222', bd=2, relief='ridge')
|
||||||
|
self.controls_frame.pack()
|
||||||
|
btn_style = {
|
||||||
|
'bg': '#333',
|
||||||
|
'fg': 'white',
|
||||||
|
'activebackground': '#555',
|
||||||
|
'activeforeground': '#00e6e6',
|
||||||
|
'font': ('Arial', 16, 'bold'),
|
||||||
|
'bd': 0,
|
||||||
|
'highlightthickness': 0,
|
||||||
|
'relief': 'flat',
|
||||||
|
'cursor': 'hand2',
|
||||||
|
'padx': 10,
|
||||||
|
'pady': 6
|
||||||
|
}
|
||||||
|
self.prev_btn = tk.Button(self.controls_frame, text='⏮ Prev', command=self.prev_media, **btn_style)
|
||||||
|
self.prev_btn.grid(row=0, column=0, padx=4)
|
||||||
|
self.pause_btn = tk.Button(self.controls_frame, text='⏸ Pause', command=self.toggle_pause, **btn_style)
|
||||||
|
self.pause_btn.grid(row=0, column=1, padx=4)
|
||||||
|
self.next_btn = tk.Button(self.controls_frame, text='Next ⏭', command=self.next_media, **btn_style)
|
||||||
|
self.next_btn.grid(row=0, column=2, padx=4)
|
||||||
|
self.settings_btn = tk.Button(self.controls_frame, text='⚙ Settings', command=self.open_settings, **btn_style)
|
||||||
|
self.settings_btn.grid(row=0, column=3, padx=4)
|
||||||
|
self.exit_btn = tk.Button(self.controls_frame, text='⏻ Exit', command=self.exit_app, **btn_style)
|
||||||
|
self.exit_btn.grid(row=0, column=4, padx=4)
|
||||||
|
self.exit_btn.config(fg='#ff4d4d')
|
||||||
|
self.controls_win.withdraw()
|
||||||
|
self.controls_win.after(200, place_controls)
|
||||||
|
self.root.bind('<Configure>', lambda e: self.controls_win.after(200, place_controls))
|
||||||
|
|
||||||
|
def hide_mouse(self):
|
||||||
|
self.root.config(cursor='none')
|
||||||
|
|
||||||
|
def show_mouse(self):
|
||||||
|
self.root.config(cursor='arrow')
|
||||||
|
|
||||||
|
def move_mouse_to_corner(self):
|
||||||
|
try:
|
||||||
|
import pyautogui
|
||||||
|
sw = self.root.winfo_screenwidth()
|
||||||
|
sh = self.root.winfo_screenheight()
|
||||||
|
pyautogui.moveTo(sw-2, sh-2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def show_controls(self):
|
def show_controls(self):
|
||||||
self.controls_frame.place(relx=0.98, rely=0.98, anchor='se')
|
self.controls_win.deiconify()
|
||||||
self.controls_frame.lift()
|
self.controls_win.lift()
|
||||||
|
self.show_mouse()
|
||||||
self.schedule_hide_controls()
|
self.schedule_hide_controls()
|
||||||
|
|
||||||
def hide_controls(self):
|
def hide_controls(self):
|
||||||
self.controls_frame.place_forget()
|
self.controls_win.withdraw()
|
||||||
|
self.hide_mouse()
|
||||||
|
|
||||||
def schedule_hide_controls(self):
|
def schedule_hide_controls(self):
|
||||||
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
|
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
|
||||||
@@ -76,13 +133,54 @@ class SimpleTkPlayer:
|
|||||||
self.root.after_cancel(self.pause_timer)
|
self.root.after_cancel(self.pause_timer)
|
||||||
self.pause_timer = None
|
self.pause_timer = None
|
||||||
|
|
||||||
|
def play_intro_video(self):
|
||||||
|
intro_path = os.path.join(os.path.dirname(__file__), 'main_data', 'intro1.mp4')
|
||||||
|
if os.path.exists(intro_path):
|
||||||
|
self.show_video(intro_path, on_end=self.after_intro)
|
||||||
|
else:
|
||||||
|
self.after_intro()
|
||||||
|
|
||||||
|
def after_intro(self):
|
||||||
|
self.show_current_media()
|
||||||
|
self.root.after(100, self.next_media_loop)
|
||||||
|
|
||||||
|
def show_video(self, file_path, on_end=None):
|
||||||
|
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||||
|
self.vlc_player.stop()
|
||||||
|
if not hasattr(self, 'video_canvas'):
|
||||||
|
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
|
||||||
|
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.label.pack_forget()
|
||||||
|
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.root.attributes('-fullscreen', True)
|
||||||
|
self.root.update_idletasks()
|
||||||
|
self.vlc_instance = vlc.Instance()
|
||||||
|
self.vlc_player = self.vlc_instance.media_player_new()
|
||||||
|
self.vlc_player.set_mrl(file_path)
|
||||||
|
self.vlc_player.set_fullscreen(True)
|
||||||
|
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
||||||
|
self.vlc_player.play()
|
||||||
|
def check_end():
|
||||||
|
if self.vlc_player.get_state() == vlc.State.Ended:
|
||||||
|
self.video_canvas.pack_forget()
|
||||||
|
self.label.pack(fill=tk.BOTH, expand=True)
|
||||||
|
if on_end:
|
||||||
|
on_end()
|
||||||
|
else:
|
||||||
|
self.root.after(200, check_end)
|
||||||
|
check_end()
|
||||||
|
|
||||||
def show_current_media(self):
|
def show_current_media(self):
|
||||||
|
self.root.attributes('-fullscreen', True)
|
||||||
|
self.root.update_idletasks()
|
||||||
if not self.playlist:
|
if not self.playlist:
|
||||||
self.label.config(text="No media available", fg='white', font=('Arial', 32))
|
self.label.config(text="No media available", fg='white', font=('Arial', 32))
|
||||||
return
|
return
|
||||||
media = self.playlist[self.current_index]
|
media = self.playlist[self.current_index]
|
||||||
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
|
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
|
||||||
if file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
if file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
||||||
|
self.show_video(file_path, on_end=self.next_media)
|
||||||
|
elif file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
||||||
try:
|
try:
|
||||||
img = Image.open(file_path)
|
img = Image.open(file_path)
|
||||||
# Fit to screen without crop or stretch
|
# Fit to screen without crop or stretch
|
||||||
@@ -115,7 +213,20 @@ class SimpleTkPlayer:
|
|||||||
self.root.after(duration * 1000, self.next_media_loop)
|
self.root.after(duration * 1000, self.next_media_loop)
|
||||||
|
|
||||||
def exit_app(self):
|
def exit_app(self):
|
||||||
self.root.destroy()
|
# Signal the update thread to stop if stop_event is present
|
||||||
|
if hasattr(self, 'stop_event') and self.stop_event:
|
||||||
|
self.stop_event.set()
|
||||||
|
if hasattr(self, 'app_running') and self.app_running:
|
||||||
|
self.app_running[0] = False
|
||||||
|
try:
|
||||||
|
if hasattr(self, 'controls_win') and self.controls_win:
|
||||||
|
self.controls_win.destroy()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.root.destroy()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def open_settings(self):
|
def open_settings(self):
|
||||||
if self.paused is not True:
|
if self.paused is not True:
|
||||||
@@ -136,6 +247,9 @@ class SimpleTkPlayer:
|
|||||||
settings_win.protocol('WM_DELETE_WINDOW', on_close)
|
settings_win.protocol('WM_DELETE_WINDOW', on_close)
|
||||||
settings_win.bind('<Destroy>', lambda e: self.resume_play() if not settings_win.winfo_exists() else None)
|
settings_win.bind('<Destroy>', lambda e: self.resume_play() if not settings_win.winfo_exists() else None)
|
||||||
|
|
||||||
|
def main_start(self):
|
||||||
|
self.play_intro_video()
|
||||||
|
|
||||||
def load_latest_playlist():
|
def load_latest_playlist():
|
||||||
files = [f for f in os.listdir(PLAYLIST_DIR) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
files = [f for f in os.listdir(PLAYLIST_DIR) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||||
if not files:
|
if not files:
|
||||||
@@ -152,6 +266,7 @@ def main():
|
|||||||
root.attributes('-fullscreen', True)
|
root.attributes('-fullscreen', True)
|
||||||
playlist = load_latest_playlist()
|
playlist = load_latest_playlist()
|
||||||
player = SimpleTkPlayer(root, playlist)
|
player = SimpleTkPlayer(root, playlist)
|
||||||
|
player.main_start()
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 MiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
BIN
signage_player/static_data/media/demo2.jpeg
Normal file
BIN
signage_player/static_data/media/demo2.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 537 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.5 MiB |
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"playlist": [
|
|
||||||
{
|
|
||||||
"file_name": "Cindrel_1.jpg",
|
|
||||||
"url": "media/Cindrel_1.jpg",
|
|
||||||
"duration": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file_name": "trans_cindrel_4.jpg",
|
|
||||||
"url": "media/trans_cindrel_4.jpg",
|
|
||||||
"duration": 10
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version": 30
|
|
||||||
}
|
|
||||||
20
signage_player/static_data/playlist/server_playlist_v4.json
Normal file
20
signage_player/static_data/playlist/server_playlist_v4.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"playlist": [
|
||||||
|
{
|
||||||
|
"file_name": "big-buck-bunny-1080p-60fps-30sec.mp4",
|
||||||
|
"url": "media/big-buck-bunny-1080p-60fps-30sec.mp4",
|
||||||
|
"duration": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "call-of-duty-black-3840x2160-23674.jpg",
|
||||||
|
"url": "media/call-of-duty-black-3840x2160-23674.jpg",
|
||||||
|
"duration": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "demo2.jpeg",
|
||||||
|
"url": "media/demo2.jpeg",
|
||||||
|
"duration": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 4
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user