commit efc80ca1e0640c64a0f8e54755a319d38c7749ed Author: wangchuanli Date: Mon Aug 24 10:51:18 2026 +0800 feat: 初始化屏幕播放控制系统 搭建基于 Flask 的多屏幕定时播放控制后端,提供设备管理控制台、播放端页面、媒体上传与配置文件持久化,并补充项目文档、示例配置及 MIT 许可证。 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) 的「常见问题」章节。*