From efc80ca1e0640c64a0f8e54755a319d38c7749ed Mon Sep 17 00:00:00 2001 From: wangchuanli Date: Mon, 24 Aug 2026 10:51:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E5=B1=8F?= =?UTF-8?q?=E5=B9=95=E6=92=AD=E6=94=BE=E6=8E=A7=E5=88=B6=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 搭建基于 Flask 的多屏幕定时播放控制后端,提供设备管理控制台、播放端页面、媒体上传与配置文件持久化,并补充项目文档、示例配置及 MIT 许可证。 --- .env.example | 7 + .gitignore | 31 ++++ LICENSE | 21 +++ config.json | 38 +++++ main.py | 210 ++++++++++++++++++++++++++ readme.md | 212 ++++++++++++++++++++++++++ requirements.txt | 2 + static/.gitkeep | 2 + templates/index.html | 337 ++++++++++++++++++++++++++++++++++++++++++ templates/player.html | 171 +++++++++++++++++++++ 用户操作指引.md | 141 ++++++++++++++++++ 11 files changed, 1172 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 config.json create mode 100644 main.py create mode 100644 readme.md create mode 100644 requirements.txt create mode 100644 static/.gitkeep create mode 100644 templates/index.html create mode 100644 templates/player.html create mode 100644 用户操作指引.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c624c37 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# 服务监听配置(复制为 .env 或直接通过环境变量设置) +# 监听地址,0.0.0.0 表示允许局域网内其他设备访问 +HOST=0.0.0.0 +# 监听端口 +PORT=5000 +# 是否开启调试模式(1 开启,0 关闭)。生产环境请设为 0 +FLASK_DEBUG=0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..712213f --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +venv/ +env/ +*.log + +# 上传的媒体文件(不纳入版本库,请用户自行放置) +static/*.mp4 +static/*.webm +static/*.ogg +static/*.mov +static/*.avi +static/*.mkv +static/*.png +static/*.jpg +static/*.jpeg +static/*.gif +static/*.webp +static/*.bmp +!static/.gitkeep + +# 本地配置(可选保留示例) +config.local.json + +# 编辑器 / 系统 +.idea/ +.vscode/ +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b847cf9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Screen Controller contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config.json b/config.json new file mode 100644 index 0000000..87ae4ec --- /dev/null +++ b/config.json @@ -0,0 +1,38 @@ +{ + "devices": [ + { + "id": "d1", + "name": "主屏幕", + "playlist": [ + { + "type": "image", + "path": "/static/image1.png", + "start_time": "2024-11-21T16:32:10", + "duration": 10 + }, + { + "type": "video", + "path": "/static/video1.mp4", + "start_time": "2024-11-21T16:55:00" + } + ] + }, + { + "id": "d2", + "name": "副屏幕", + "playlist": [ + { + "type": "image", + "path": "/static/image1.png", + "start_time": "2024-11-21T16:26:10", + "duration": 10 + }, + { + "type": "video", + "path": "/static/video2.mp4", + "start_time": "2024-11-21T16:40:00" + } + ] + } + ] +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..b400acd --- /dev/null +++ b/main.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +屏幕播放控制系统 - 后端服务 +Screen Controller - Backend Server + +一个用于展厅多屏幕定时播放控制的轻量级系统: +- 控制端(/)通过 Web 界面管理设备与播放列表 +- 播放端(/player?device_id=xxx)按配置的时间定时播放图片/视频 +- 支持局域网内多台设备同时访问 +""" + +import os +import json +from datetime import datetime + +from flask import ( + Flask, + render_template, + request, + jsonify, + send_from_directory, +) +from werkzeug.utils import secure_filename + +app = Flask(__name__) + +# --------------------------------------------------------------------------- +# 基础配置 +# --------------------------------------------------------------------------- +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +CONFIG_FILE = os.path.join(BASE_DIR, "config.json") +STATIC_DIR = os.path.join(BASE_DIR, "static") +UPLOAD_DIR = STATIC_DIR # 上传的媒体文件存放目录 + +# 允许上传的媒体类型(扩展名 -> 类型) +ALLOWED_EXTENSIONS = { + "png": "image", + "jpg": "image", + "jpeg": "image", + "gif": "image", + "webp": "image", + "bmp": "image", + "mp4": "video", + "webm": "video", + "ogg": "video", + "mov": "video", + "avi": "video", + "mkv": "video", +} + +# 默认监听地址与端口(0.0.0.0 允许局域网访问) +HOST = os.environ.get("HOST", "0.0.0.0") +PORT = int(os.environ.get("PORT", "5000")) + + +# --------------------------------------------------------------------------- +# 配置读写 +# --------------------------------------------------------------------------- +def load_config(): + """加载配置文件;若不存在则返回默认结构。""" + if os.path.exists(CONFIG_FILE): + try: + with open(CONFIG_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + if not isinstance(data, dict): + data = {} + if "devices" not in data or not isinstance(data["devices"], list): + data["devices"] = [] + return data + except (json.JSONDecodeError, OSError): + return {"devices": []} + return {"devices": []} + + +def save_config(config): + """保存配置到文件。""" + with open(CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(config, f, ensure_ascii=False, indent=4) + + +def find_device(config, device_id): + """在配置中查找指定 id 的设备。""" + return next((d for d in config.get("devices", []) if d.get("id") == device_id), None) + + +def allowed_file(filename): + """判断文件扩展名是否在允许列表中。""" + return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS + + +# --------------------------------------------------------------------------- +# 页面路由 +# --------------------------------------------------------------------------- +@app.route("/") +def index(): + """控制端:设备管理页面。""" + return render_template("index.html") + + +@app.route("/player") +@app.route("/player/") +def player(device_id=None): + """播放端页面。device_id 既可通过路径也可通过查询参数传入。""" + return render_template("player.html") + + +# --------------------------------------------------------------------------- +# API:配置 +# --------------------------------------------------------------------------- +@app.route("/get_config") +def get_config(): + """返回完整 JSON 配置。""" + return jsonify(load_config()) + + +@app.route("/save_config", methods=["POST"]) +def save_config_endpoint(): + """保存完整配置(来自控制端编辑)。""" + try: + data = request.get_json(force=True, silent=True) + if not isinstance(data, dict) or "devices" not in data: + return jsonify({"status": "error", "message": "配置格式不正确"}), 400 + save_config(data) + return jsonify({"status": "success"}) + except Exception as e: # noqa: BLE001 + return jsonify({"status": "error", "message": str(e)}), 400 + + +@app.route("/get_playlist/") +def get_playlist(device_id): + """播放端请求某个设备的播放列表。""" + config = load_config() + device = find_device(config, device_id) + if not device: + return jsonify({"error": "Device not found"}), 404 + # 按 start_time 升序返回,便于前端调度 + playlist = sorted( + device.get("playlist", []), + key=lambda x: x.get("start_time", ""), + ) + return jsonify( + { + "device_id": device_id, + "name": device.get("name", device_id), + "playlist": playlist, + } + ) + + +# --------------------------------------------------------------------------- +# API:媒体文件 +# --------------------------------------------------------------------------- +@app.route("/upload", methods=["POST"]) +def upload_file(): + """上传媒体文件到 static 目录。""" + if "file" not in request.files: + return jsonify({"status": "error", "message": "未找到文件字段"}), 400 + file = request.files["file"] + if file.filename == "": + return jsonify({"status": "error", "message": "未选择文件"}), 400 + if not allowed_file(file.filename): + return jsonify({"status": "error", "message": "不支持的文件类型"}), 400 + + filename = secure_filename(file.filename) + # 避免重名覆盖 + base, ext = os.path.splitext(filename) + counter = 1 + target = os.path.join(UPLOAD_DIR, filename) + while os.path.exists(target): + filename = f"{base}_{counter}{ext}" + target = os.path.join(UPLOAD_DIR, filename) + counter += 1 + + file.save(target) + media_type = ALLOWED_EXTENSIONS[filename.rsplit(".", 1)[1].lower()] + return jsonify( + { + "status": "success", + "filename": filename, + "path": f"/static/{filename}", + "type": media_type, + } + ) + + +@app.route("/files") +def list_files(): + """列出 static 目录下可用的媒体文件。""" + files = [] + if os.path.isdir(STATIC_DIR): + for name in sorted(os.listdir(STATIC_DIR)): + ext = name.rsplit(".", 1)[-1].lower() if "." in name else "" + if ext in ALLOWED_EXTENSIONS: + files.append({"name": name, "path": f"/static/{name}", "type": ALLOWED_EXTENSIONS[ext]}) + return jsonify({"files": files}) + + +@app.route("/static/") +def serve_static(filename): + """提供静态媒体文件访问。""" + return send_from_directory(STATIC_DIR, filename) + + +if __name__ == "__main__": + os.makedirs(STATIC_DIR, exist_ok=True) + # 关闭 debug 以避免生产环境安全隐患;可通过环境变量 FLASK_DEBUG=1 开启 + debug = os.environ.get("FLASK_DEBUG", "0") == "1" + print(f"屏幕控制系统已启动: http://{HOST}:{PORT}/ (局域网内其他设备可访问)") + app.run(host=HOST, port=PORT, debug=debug) diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..510bf88 --- /dev/null +++ b/readme.md @@ -0,0 +1,212 @@ +# 屏幕播放控制系统 / Screen Controller + +> 一个轻量级的展厅多屏幕定时播放控制系统,方便展厅介绍员通过 Web 控制台统一管理多台显示屏的定时图片/视频播放。 + +A lightweight, web-based multi-screen scheduled playback control system for exhibition halls. It lets presenters centrally manage scheduled image/video playback across multiple displays from a single control console. + +--- + +## 功能特性 + +- **多设备管理**:在一台控制端集中管理多块屏幕(设备),每块屏幕对应一个独立播放列表。 +- **定时播放**:为每条内容设置开始时间,播放端到点自动切换;支持图片(可设展示秒数)与视频。 +- **可视化控制台**:无需手改 JSON,通过网页即可新增/删除设备、编辑播放列表、上传媒体文件。 +- **媒体库**:支持在控制台直接上传图片/视频,并一键插入到播放列表。 +- **局域网部署**:默认监听 `0.0.0.0`,展厅内任意设备(电脑、电视盒子、平板)通过浏览器访问播放端即可。 +- **实时轮询**:播放端每 30 秒从服务端拉取最新配置,改完即生效,无需刷新。 + +--- + +## 系统架构 + +``` +控制端 (浏览器) ──HTTP──> Flask 服务 <──HTTP── 播放端 (各屏幕浏览器) + / | config.json /player?device_id=xx + 管理设备/播放列表/上传 媒体文件 (static/) +``` + +- **控制端** `/`:展厅介绍员使用的管理页面。 +- **播放端** `/player?device_id=xxx`:部署在每块屏幕的浏览器上,按配置定时播放。 +- **数据** `config.json`:设备与播放列表的持久化存储。 +- **媒体** `static/`:图片与视频文件存放目录。 + +--- + +## 快速开始 + +### 1. 环境要求 + +- Python 3.8+ +- 支持现代浏览器的播放设备(建议 Chrome / Edge) + +### 2. 安装 + +```bash +# 克隆仓库(开源后) +git clone +cd screen_controller + +# 创建虚拟环境(可选但推荐) +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate + +# 安装依赖 +pip install -r requirements.txt +``` + +### 3. 运行 + +```bash +python main.py +``` + +启动后控制台会输出访问地址,默认: + +- 控制端: `http://0.0.0.0:5000/` +- 播放端示例: `http://<服务器IP>:5000/player?device_id=d1` + +> 局域网内其他设备请使用**服务器的内网 IP**(如 `http://192.168.1.100:5000/`)访问。 + +### 4. 环境变量(可选) + +可通过环境变量自定义监听配置(参见 `.env.example`): + +| 变量 | 说明 | 默认值 | +| --- | --- | --- | +| `HOST` | 监听地址,`0.0.0.0` 允许局域网访问 | `0.0.0.0` | +| `PORT` | 监听端口 | `5000` | +| `FLASK_DEBUG` | 调试模式,`1` 开启 | `0` | + +--- + +## 使用说明 + +### 控制端 + +1. 打开 `http://<服务器IP>:5000/`,左侧为设备列表。 +2. 点击「+ 新增」创建设备,填写设备名称与 ID。 +3. 在「播放列表」中添加条目: + - **类型**:图片 / 视频 + - **路径**:媒体文件在 `static/` 下的访问路径,如 `/static/video1.mp4` + - **开始时间**:该内容开始播放的时间 + - **时长(秒)**:仅对图片有效,默认 10 秒 +4. 「媒体库」区域可直接上传本地图片/视频,上传后点击「使用」即可插入到播放列表。 +5. 点击「保存」后配置立即生效,播放端会在 30 秒内自动更新。 + +### 播放端 + +在每块屏幕上用浏览器打开对应的播放端地址,例如: + +``` +http://<服务器IP>:5000/player?device_id=d1 +``` + +建议将播放端浏览器设为**全屏**(F11)。播放端会: +- 按各条目 `start_time` 倒计时到点播放; +- 视频播完后自动切换到下一条; +- 图片按 `duration` 秒数展示后切换; +- 每 30 秒拉取最新配置,后台修改无需人工刷新。 + +--- + +## 配置文件格式 + +`config.json` 结构如下(也可通过控制端界面编辑,无需手写): + +```json +{ + "devices": [ + { + "id": "d1", + "name": "主屏幕", + "playlist": [ + { + "type": "image", + "path": "/static/image1.png", + "start_time": "2024-11-21T16:32:10", + "duration": 10 + }, + { + "type": "video", + "path": "/static/video1.mp4", + "start_time": "2024-11-21T16:55:00" + } + ] + } + ] +} +``` + +| 字段 | 说明 | +| --- | --- | +| `devices[].id` | 设备唯一标识,播放端 URL 中 `device_id` 对应此值 | +| `devices[].name` | 设备显示名称 | +| `devices[].playlist[].type` | `image` 或 `video` | +| `devices[].playlist[].path` | 媒体文件访问路径(位于 `static/` 下) | +| `devices[].playlist[].start_time` | 开始播放时间(ISO 8601,本地时间) | +| `devices[].playlist[].duration` | 图片展示秒数(视频忽略) | + +--- + +## 目录结构 + +``` +screen_controller/ +├── main.py # Flask 后端服务 +├── config.json # 设备与播放列表配置 +├── requirements.txt # Python 依赖 +├── .env.example # 环境变量示例 +├── static/ # 媒体文件存放目录(图片/视频) +└── templates/ + ├── index.html # 控制端页面 + └── player.html # 播放端页面 +``` + +--- + +## 文档导航 + +- [用户操作指引](./用户操作指引.md):面向展厅介绍员/运维人员的日常使用手册(快速上手、常见场景、状态说明、排错)。 +- 本文档(README):系统介绍、部署与技术说明。 + +--- + +## 常见问题 + +**Q: 播放端打不开 / 一直「正在初始化」?** +- 确认设备 ID 与 `config.json` 中一致; +- 确认访问地址使用的是服务器**内网 IP** 而非 `localhost`(localhost 仅本机有效)。 + +**Q: 视频无法自动播放?** +- 部分浏览器禁止有声自动播放,展厅设备建议在系统设置中允许自动播放,或保持浏览器在前台。 + +**Q: 修改配置后播放端没变化?** +- 播放端每 30 秒轮询一次,最多等待 30 秒生效;也可手动刷新播放端页面。 + +**Q: 时间不准确?** +- 播放以**播放设备本地系统时间**为准,请保证各屏幕设备时间同步(建议开启网络时间同步)。 + +--- + +## 开源协议 + +本项目基于 [MIT License](./LICENSE) 开源,可自由用于学习、商用与二次开发,请保留版权声明。 + +--- + +## English + +**Screen Controller** is a lightweight web system for scheduling image/video playback across multiple screens in an exhibition hall. + +- **Console** (`/`): manage devices, playlists, and upload media via a web UI. +- **Player** (`/player?device_id=xxx`): runs on each screen's browser and plays content by schedule. +- **Backend**: Flask serves config from `config.json` and media from `static/`. + +Quick start: + +```bash +pip install -r requirements.txt +python main.py +``` + +Then open `http://:5000/` for the console, and `http://:5000/player?device_id=d1` on each display. Licensed under the MIT License. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e9e66c9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask>=3.0.0 +Werkzeug>=3.0.0 diff --git a/static/.gitkeep b/static/.gitkeep new file mode 100644 index 0000000..ffce464 --- /dev/null +++ b/static/.gitkeep @@ -0,0 +1,2 @@ +# 该目录用于存放上传/播放的媒体文件(图片、视频)。 +# 实际媒体文件已被 .gitignore 忽略,请勿提交大体积文件。 diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..946fa06 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,337 @@ + + + + + + 屏幕播放控制系统 - 控制台 + + + +
+

屏幕播放控制系统

+ 展厅多屏定时播放控制台 +
+ +
+ + + + +
+
请选择左侧设备,或点击「新增」创建设备。
+
+
+ +
+ + + + diff --git a/templates/player.html b/templates/player.html new file mode 100644 index 0000000..99be1e8 --- /dev/null +++ b/templates/player.html @@ -0,0 +1,171 @@ + + + + + + 定时播放器 + + + +
+
+
+
+

正在初始化...

+
+ + + + diff --git a/用户操作指引.md b/用户操作指引.md new file mode 100644 index 0000000..9181dba --- /dev/null +++ b/用户操作指引.md @@ -0,0 +1,141 @@ +# 屏幕播放控制系统 · 用户操作指引 + +> 本指引面向**展厅介绍员 / 运维人员**,帮助你快速上手系统的日常使用。 +> 技术部署相关内容请参见 [README.md](./README.md)。 + +--- + +## 一、系统角色说明 + +本系统有两套界面,分别用于「管理」和「播放」: + +| 界面 | 地址 | 使用者 | 用途 | +| --- | --- | --- | --- | +| **控制端(控制台)** | `http://<服务器IP>:5000/` | 展厅介绍员 | 管理设备、编排播放列表、上传媒体 | +| **播放端** | `http://<服务器IP>:5000/player?device_id=xxx` | 每块屏幕 | 按时间自动播放图片/视频 | + +> 说明:`<服务器IP>` 指运行 `main.py` 的那台电脑的内网 IP(如 `192.168.1.100`)。 +> 控制端和播放端可以运行在**不同设备**上,只要在同一局域网即可。 + +--- + +## 二、首次使用流程 + +``` +1. 启动服务 → 2. 打开控制端 → 3. 创建设备 → 4. 上传媒体 + → 5. 编排播放列表 → 6. 各屏幕打开播放端 → 7. 日常维护 +``` + +### 步骤 1:启动服务 +在服务器上执行: +```bash +python main.py +``` +看到 `屏幕控制系统已启动: http://0.0.0.0:5000/` 即表示成功。 + +### 步骤 2:打开控制端 +在任意同一局域网的电脑/平板浏览器中访问: +``` +http://<服务器IP>:5000/ +``` + +### 步骤 3:创建设备 +1. 点击左侧「**+ 新增**」按钮,系统自动生成一个设备(如 `d1`)。 +2. 在右侧「设备名称」填写便于识别的名字(如「入口主屏」)。 +3. 点击「**保存设备信息**」。 + +> 每块实体屏幕对应一个设备,设备 ID(如 `d1`)将在播放端地址中使用,请勿重复。 + +### 步骤 4:上传媒体文件 +1. 在页面下方「**媒体库**」区域点击「**上传媒体**」。 +2. 选择本地图片(png/jpg 等)或视频(mp4/webm 等)。 +3. 上传成功后文件出现在媒体库中,可点击「**使用**」直接加入当前设备的播放列表。 + +> 也可不通过媒体库,手动在播放列表中填写 `static/` 下的文件路径。 + +### 步骤 5:编排播放列表 +在「播放列表」区域: +1. 点击「**+ 添加条目**」新增一条播放内容。 +2. 填写每条内容的参数: + +| 参数 | 说明 | 示例 | +| --- | --- | --- | +| 类型 | 图片 / 视频 | `image` | +| 路径 | 媒体在 `static/` 下的访问地址 | `/static/video1.mp4` | +| 开始时间 | 该内容**开始播放**的时间(本地时间) | `2026-08-24T16:32` | +| 时长(秒) | 仅图片有效,到时自动切换 | `10` | + +3. 多条内容按「开始时间」从早到晚自动排序播放。 +4. 编辑完成后点击「**保存播放列表**」。 + +### 步骤 6:各屏幕打开播放端 +在每块展示屏幕的浏览器中访问对应地址(设备 ID 对应步骤 3 的值): +``` +http://<服务器IP>:5000/player?device_id=d1 +``` +建议按 **F11** 进入全屏,并固定该页面不关闭。 + +### 步骤 7:日常维护 +- 修改配置后**无需重启播放端**:播放端每 30 秒自动拉取最新配置。 +- 紧急情况可手动刷新播放端页面(F5)立即生效。 + +--- + +## 三、常见操作场景 + +### 场景 A:临时插播一段视频 +1. 控制端 → 媒体库「上传媒体」上传视频。 +2. 播放列表「+ 添加条目」→ 类型选视频 → 路径选刚上传的文件 → 开始时间设为「现在或稍后」。 +3. 保存。对应屏幕会在到点后自动播放。 + +### 场景 B:更换某块屏幕的全部内容 +直接在控制端选中该设备,清空或重新编排播放列表后保存即可。 + +### 场景 C:新增一块屏幕 +1. 控制端「+ 新增」创建设备,记住其 ID。 +2. 新屏幕浏览器打开 `/player?device_id=新ID`。 +3. 编排该设备播放列表并保存。 + +### 场景 D:图片轮播展示 +将多条图片条目依次排列,每条设置合适的「时长(秒)」,系统会按时间顺序、各自展示设定秒数后切换。 + +--- + +## 四、播放端状态说明 + +播放端页面右下角会显示当前状态,含义如下: + +| 状态文字 | 含义 | +| --- | --- | +| `正在初始化...` | 正在从服务端获取配置 | +| `即将播放: xxx @ 时间` | 已排到下一条,正在倒计时 | +| `正在播放: xxx` | 当前正在展示该内容 | +| `当前没有待播放内容` | 配置中所有条目的开始时间都已过去 | +| `获取配置失败:设备不存在` | `device_id` 在配置中找不到,请核对 | +| `请在 URL 中指定 device_id...` | 打开播放端时未带 `?device_id=`,请补全 | + +> 播放端右上角有时钟,用于核对**本地系统时间**是否与标准时间一致(播放以屏幕本机时间为准)。 + +--- + +## 五、注意事项与排错 + +1. **时间同步**:各屏幕设备的系统时间应保持一致(建议开启网络时间同步),否则定时播放会偏差。 +2. **访问地址**:播放端必须用**服务器内网 IP** 访问,不能用 `localhost`(`localhost` 只指向本机)。 +3. **自动播放限制**:部分浏览器禁止带声音的视频自动播放,展厅设备建议在浏览器设置中允许自动播放,或保持页面在前台。 +4. **媒体文件**:上传的视频建议控制在合理体积,避免网络传输卡顿;文件存于 `static/` 目录。 +5. **配置备份**:`config.json` 保存了全部设备与播放列表,定期备份该文件以防丢失。 + +--- + +## 六、快捷键速查 + +| 操作 | 快捷键 | +| --- | --- | +| 浏览器全屏(推荐播放端使用) | `F11` | +| 刷新播放端立即生效 | `F5` | +| 控制台保存 | 页面内「保存」按钮 | + +--- + +*如遇到本指引未覆盖的问题,请查阅 [README.md](./README.md) 的「常见问题」章节。*