局域网流媒体播放器

fplayer-ff-mobile 技术全解

一份逐词解释的技术教程。不预设任何知识,从零开始讲清楚每一个术语。

📱 Android + Windows
🎬 HLS / FLV / RTMP
🏗️ Flutter + Dart
局域网低延迟

功能模块索引

单文件架构 (main.dart ~1,400行),按 14 个功能领域分段讲解

01 · 项目概览

📖 先理解:什么是 App?什么是"程序"?

你手机上的每一个图标——微信、抖音、地图——点开之后运行的那个东西,就是一个 App(Application,应用程序)。App 的本质是一堆指令(告诉手机"在屏幕上画一个按钮"、"收到消息时响一声"),由程序员用一种叫编程语言的特殊文字写出来。手机里的芯片(处理器)能读懂这些指令并执行。

不同的手机需要不同的指令格式:Android 手机和 iPhone 用的不是同一种"语言",所以同一个 App 通常需要写两遍。但有一种技术可以只写一遍、自动翻译到多个平台,这就是 Flutter

一句话定义:fplayer-ff-mobile 是一个手机/电脑上运行的 App,用来播放同一局域网内其他设备发出的实时视频画面。

📖 先理解:什么是局域网(LAN)?

当你把手机和电脑连到同一个 WiFi,或者用网线插到同一个路由器上时,这些设备就组成了一个局域网(Local Area Network,简称 LAN)。

局域网里的设备可以直接互相"对话"——数据只在本地这个小圈子里流动,所以速度特别快、延迟特别低(延迟就是从"A发出数据"到"B收到数据"之间的时间差)。本项目就是利用了这个特性——视频画面在局域网内直接传输,几乎感觉不到卡顿。局域网不需要互联网,即使你家断网了,局域网里的设备依然可以互相通信。

📖 先理解:什么是视频流(Streaming)?什么是"拉流"?

你在优酷或 B站 看视频时,视频文件存在网站的服务器上。你看的时候,服务器把数据一段一段地发给你,手机收到一段就播放一段——这就是流媒体。好处是不用等整个视频下载完才能看。

"拉流"就是从别人那里把视频流"拉"过来播放(你是接收方)。"推流"就是你把自己的视频"推"出去给别人看(你是发送方)。本 App 的角色是拉流——从别人(Desktop)那里拉视频过来播放。

核心技术事实

维度具体内容
编程语言Dart(Google 设计的一种语言,专门配合 Flutter 框架使用)
使用的框架Flutter(Google 的跨平台开发框架——写一遍代码,安卓和 Windows 都能跑)
界面规范Material Design 3(Google 发布的界面设计系统)
播放核心media_kit(在 Flutter 和 libmpv 播放引擎之间搭桥的中间包)
网络通信HTTP(网页浏览用的通信方式)、TCP(更底层的数据传输方式)
本地存储SharedPreferences(一种简单的小数据存储方案,存用户设置)
能跑在哪些设备上Android 手机、Windows 电脑(Web 平台代码已预留但待构建配置)

02 · 三端协同生态

本项目不是独立运行的——它需要另外两个程序配合,形成一条完整的"视频生产→分发→消费"链路。

🖥️ fplayer-ff-desktop 视频生产者 采集画面 → 编码 → 推送 注册流 ⚙️ fplayer-ff-service 调度中心 注册流 → 管理地址 → 响应查询 查询 📱 fplayer-ff-mobile 👈 本项目 查询地址 → 拉流 → 解码 → 播放 拿到地址后直接向 Desktop 拉流 视频数据直接传输(不经过 Service)
▲ 三端协作流程:Service 只管"查地址",视频数据在 Desktop 和 Mobile 之间直接传输
📖 先理解:什么是 IP 地址?什么是端口?

局域网中每台设备都有一个IP 地址——就是这台设备在网络上的"门牌号"。比如 192.168.1.5 就代表某台设备。端口是一个数字编号(0到65535),标识这台设备上的具体网络程序。IP地址 + 端口 = 精确位置。比如 192.168.1.5:8080 就是"那台设备上的8080端口"。

网络地址 = IP地址(找到设备) + 端口(找到设备上的程序) 🖥️ 设备 A(Desktop) IP: 192.168.1.5 端口 8080 流媒体服务 端口 9000 Service 通过 IP 找到设备 📱 设备 B(Mobile) IP: 192.168.1.8 没有服务在监听 Mobile 向 192.168.1.5:8080 发起连接 🔢 关键概念 同一设备可同时运行 多个网络程序—— 各占不同端口,互不干扰 端口范围:0 ~ 65535
▲ IP 找到设备,端口找到设备上的具体程序。同一设备可以同时跑多个服务(8080端口跑流媒体,9000端口跑Service),互不冲突
🖥️

Desktop(推流端)

运行在电脑上。采集画面(屏幕录制或摄像头),用数学算法压缩后发送到网络中。发送前会在 Service 上登记自己的存在。

⚙️

Service(调度端)

一台常驻运行的服务程序。它维护一份"谁在播、播到哪"的清单。如果端口变化了它知道最新的,Mobile 不需要关心变化。

📱

Mobile(播放端 · 本项目)

用户打开 App,输入 Service 地址 + 流名。App 自动从 Service 查出流的实际播放地址,然后开始拉数据、解压缩、显示到屏幕上。

为什么需要 Service? 没有 Service,Mobile 得直接知道 Desktop 的 IP 和端口。端口可能随时变(被别的程序占了就换一个)。Service 就是一个"电话号码本"——Desktop 每次换端口都告诉 Service,Mobile 先查本子再拨号。这样 Mobile 只需记住固定的 Service 地址和流名。

03 · 播放流程

从点击"播放"到画面出现,经历了5个步骤。

1 验证输入 检查必填项 为空则立即报错 2 解析地址 向Service查询 得到真实播放URL 3 兼容检查 Web拦截FLV/RTMP Android仅警告 4 引擎接管 libmpv开始工作 接收→解码→渲染 5 状态反馈 LIVE徽标+日志 实时更新
▲ 播放流程:5步,从输入验证到画面播放

技术栈分层视图

App 内部按"分层"组织——每层只解决一类问题,只和上下邻居交互。好处是:想换播放器?只改播放器层,其他层不动。

🎨 UI 层按钮、卡片、输入框、视频画面、日志——用户看到和碰到的一切Flutter Widget
🧠 逻辑层播放/停止控制、地址解析、状态管理、输入验证Dart · setState
📡 网络层向 Service 发 HTTP 请求、接收视频流数据http · TCP
💾 存储层用户设置(主题、URL、协议)的保存和读取SharedPreferences
🎬 播放器层封装 libmpv 引擎,提供跨平台解码和渲染media_kit · libmpv
⚙️ 原生平台层Android (Kotlin+Gradle)、Windows (C++/CMake)Kotlin · C++
📖 KOTLIN、C++、JAVASCRIPT 是什么?

这些都是编程语言。不同语言适合干不同的事:Kotlin 是 Android 系统的最佳搭档;C++ 性能极高,适合视频解码这种重体力活;C++ 性能极高,适合视频解码这种重体力活。Flutter 通过平台通道与原生层通信,你不需要单独学它们。

04 · 流媒体传输协议

📖 先理解:视频文件有多大?为什么必须压缩和拆分?

一段1分钟、1080p清晰度的原始(未压缩)视频,大小大约是 10GB。如果你家网速是100Mbps,下载10GB要花约14分钟——而你只想看1分钟的视频。所以视频在传输前必须被压缩(用数学算法删掉人眼不太敏感的细节),10GB压到几十MB。即使压缩后还是很大,没法等全下载完再播——这就引出了"怎么传"的问题。不同的传法,就是不同的协议

HLS

HTTP Live Streaming

做法:把视频切成小片段(每个2-10秒,存为 .ts 文件),同时写一份"播放列表"(.m3u8),告诉播放器各片段的网址顺序。

过程:先下载列表 → 按序逐个下载片段 → 播完一个下下一个。

优点:所有浏览器和手机天生支持。
缺点:延迟2-5秒,因为要等片段完整生成才能下载。

✅ Web 和 Android 首选

HTTP-FLV

Flash Video over HTTP

做法:不切片。建立连接后,数据连续不断地推过来,收到一段立刻解码播放一段。

过程:连上 → 数据持续到达 → 边收边播,不等。

优点:延迟极低(不到1秒),不需等片段。
缺点:浏览器不原生支持 FLV 解码。

✅ Windows 桌面首选

RTMP

Real-Time Messaging Protocol

做法:用专用端口1935,建立一条持续的双向通道,数据能同时往两个方向流动。

过程:连接 → 双方握手 → 沿通道持续推送。

优点:延迟极低,支持双向。
缺点:2020年后浏览器不再支持。

⚠️ 仅特殊场景

HLS: 片1.ts 片2.ts 片3.ts ...连续片段 每个片段先完整生成 → 才能被下载 → 延迟2-5秒 HTTP-FLV: 连续数据流 ···················· 边收边播,不等 不切片 → 无需等待 → 延迟<1秒 RTMP: 端口1935 双向持续通道(推+拉同时) 独有端口 → 浏览器不支持
▲ 三种协议的数据传输方式对比:切片 vs 连续流 vs 双向通道

各平台的处理策略

平台默认推荐FLV/RTMP 行为
WindowsHTTP-FLV✅ 正常播放
AndroidHLS⚠️ 弹出警告,但不阻止
WebHLS(仅此选项)❌ 直接拦截,提示切换 HLS

05 · 播放器引擎

📖 先理解:压缩(编码)和解压缩(解码)

编码(压缩):发送端拿到原始画面,用数学算法分析"哪些信息可以丢掉而不被人眼察觉"。比如天空区域相邻像素颜色差不多就只存一个代表色;连续两帧之间背景没动就只存变化的部分。10GB → 30MB。

解码(解压缩):接收端把压缩数据还原为可显示的像素。这不是简单的"反向运行"——需要大量数学运算来推算被删掉的细节。这个过程极复杂、极消耗算力,必须用专门的程序处理——这就是播放器引擎的核心工作。

🎬 原始视频 1920×1080 像素 1分钟 ≈ 10 GB 太大,无法传输 编码 🗜️ 压缩算法 分析画面 删冗余信息 (人眼不敏感的细节) 📦 压缩后视频 1分钟 ≈ 30 MB 可以网络传输了 网络 传输 ☁️ 到达
▲ 编码(压缩):10GB → 30MB,删掉人眼不敏感的信息。解码(解压):30MB → 还原为可显示的像素。解码的计算量远大于编码
📖 什么是"程序库"(Library)?

在编程中,就是别人写好的一堆功能,打包好给你直接用。你不需要从零写"怎么解压视频"——已经有人写好了 libmpv,你直接调用就行。libmpv 来自 mpv(一个著名的开源播放器软件),被提取出来做成独立的库,任何程序都可以调用它来获得视频播放能力。

Flutter App Dart 语言 "播放这个URL" "画面画到这块区域" 🧩 media_kit 翻译官 / 桥接层 Dart ↔ C 互译 🎬 libmpv C 语言 接收网络数据 解码 → 渲染像素 不懂 C,无法直接 调用 libmpv media_kit 做中间翻译 不懂 Dart,无法直接 被 Flutter 调用
▲ media_kit 的桥接角色:Flutter (Dart) 和 libmpv (C) 语言不通,media_kit 在中间翻译
🧩

media_kit(核心包)

负责 Dart ↔ C 之间的通信翻译。把 Flutter 的命令(打开、播放、暂停、停止)转发给 libmpv,把 libmpv 的输出传回 Flutter。

🎥

media_kit_video

负责把 libmpv 解码出的像素数据画到屏幕上的指定区域。同时提供播放控件界面,并自动适配不同平台。

📦

media_kit_libs_video

包含 libmpv 在各平台上的预编译文件。Android 用 .so 格式,Windows 用 .dll 格式(不同操作系统的库文件格式)。

06 · 服务通信(Gateway API)

📖 HTTP 是什么?GET 请求是什么?JSON 是什么?

HTTP:互联网上最基础的通信规则——你每次打开网页就在用它。一方发请求("给我某某数据"),另一方回响应("给,这是数据")。GET 是最常用的请求类型——纯查询,不修改任何东西。参数直接附在网址后面:用 ? 开始,& 分隔。

JSON:一种用纯文本表示数据的格式。花括号 {} 包住"对象"(名字:值的配对),方括号 [] 包住"列表"。几乎所有编程语言都能直接读懂,所以互联网上程序间传数据绝大多数都用它。

📱 客户端 (Mobile) 发起 HTTP 请求 "我想要数据" IP: 192.168.1.8 ① GET 请求 /api/v1/streams/resolve ?app=live&stream=001 ⚙️ 服务端 (Service) 收到请求 查询 → 返回数据 IP: 192.168.1.5 ② JSON 响应 {playHls: "...", ...} 关键点 GET = 纯查询 不修改服务端数据 参数附在URL后面 ? 开始,& 分隔
▲ HTTP 通信模型:客户端发请求,服务端回响应。GET 只查询不修改,参数直接附在网址后面
// Mobile 向 Service 发出的实际请求 GET http://192.168.1.100:9000/api/v1/streams/resolve?app=live&stream=stream001 // 翻译成人话:去192.168.1.100的9000端口, // 找/api/v1/streams/resolve这个路径, // 查app=live、stream=stream001这条流 // Service 返回的 JSON 响应: { "playHls": "http://192.168.1.100:8080/live/stream001.m3u8", "playHttpFlv": "http://192.168.1.100:8080/live/stream001.flv", "playUrls": { "rtmp": "rtmp://192.168.1.100:1935/live/stream001", "hls": "http://192.168.1.100:8080/live/stream001.m3u8" } }

地址提取逻辑

用户选了App 优先取哪个字段如果为空,回退取哪个
HLSplayHlsplayUrls.hls
HTTP-FLVplayHttpFlv(无回退,为空就报错)
RTMPplayUrls.rtmp(无回退,为空就报错)

两种使用模式

服务模式直接模式
地址怎么来的通过上面讲的 API 自动查用户自己手打完整网址
用户需要填什么Service地址 + App名 + 流名完整的播放网址
端口变了怎么办✅ 自动跟上❌ 得手动去改网址

07 · UI 与主题系统

📖 什么是 UI?什么是"设计系统"?

UI(用户界面)就是你能看到和触碰到的所有界面元素——按钮、输入框、颜色、字体。UI 的好坏决定了你用 App 时觉得"舒服"还是"别扭"。设计系统是一套事先定好的规则——"按钮用这个色、卡片用这个圆角"。有了规则,程序员不用每做一个新页面都重新想一遍,直接按规则来。换整套规则就换整套外观。

📖 Material Design 和"设计令牌"是什么?

Material Design 是 Google 发布的界面设计规范——"App 长相说明书"。2021年的第三版(Material 3)引入了设计令牌:给颜色/字体/形状起名字——"主色"叫 primary、"表面色"叫 surface。开发者只给这些名字赋值一次,整个 App 的所有组件就自动统一换色。比每个按钮单独设色高效得多。

设计令牌定义 primary = #17171C surface = #FFFFFF error = #B30000 card-radius = 22px button-radius = 32px 🔘 按钮 bg=primary, r=32px 🃏 卡片 bg=surface, r=22px 📝 输入框 focus=formFocus ⚠️ 错误 color=error 📋 AppBar bg=primaryContainer 🔗 链接文字 color=secondary 🔴 LIVE徽标 color=coral ↑ 所有组件自动从令牌取值,改一处令牌值 → 全局统一换色
▲ 设计令牌一次定义、全局生效:改 primary 的颜色值,所有按钮、链接、图标自动跟变

本项目的实际配色

用途色值出现位置
主色#17171C按钮、标题文字、重要图标
辅色#1863DC链接文字、播放中蓝色高亮、INFO 标签
强调色#FF7759LIVE 直播徽标、WARN 警告标签
AppBar 背景#003C33顶栏底色
输入框焦点#9B60AA点输入框时,边框变这个颜色
卡片圆角22px所有卡片的统一圆角
按钮圆角32px所有按钮的统一药丸形

08 · 设置持久化

📖 内存 vs 硬盘——数据存在哪?

内存(RAM):运行中的程序数据存在这里。极快(纳秒级读写),但断电就没了。App 一关,内存被回收,数据全清。硬盘/闪存(Disk/Flash):永久存储。写起来比内存慢,但关机关 App 都不丢持久化就是把数据从"一关就丢"的内存搬到"关了还在"的硬盘上。

内存 RAM 速度:极快(纳秒级) 致命弱点:断电即丢 App关闭 → 数据全部清空 用户填的配置全没了 ✗ 持久化 写入 💾 硬盘/闪存 Disk 速度:比内存慢 优势:永久保存 关机、关App → 数据还在 下次打开App配置全在 ✓ SharedPreferences Flutter内置 "共享偏好存储" gateway_url → "http://..." theme_mode → "dark" app → "live"
▲ 持久化的本质:把数据从"一关就没"的内存转移到"关了还在"的硬盘。SharedPreferences 是 Flutter 内置的小数据存储方案
📖 键值对存储(Key-Value)是什么?

最简单的数据存储方式。每条数据由名字(键)和内容(值)组成。存:把键"gateway_url"对应的值存为"http://192.168.1.100:9000"。取:给我键"gateway_url"对应的值。极快、极简单,适合少量配置信息。数据量大应该用数据库。SharedPreferences 就是 Flutter 和 Android 内置的键值对存储。

本项目的存储设计

自动保存:每次用户打字,Flutter 监听机制自动触发保存。启动恢复:App 启动时读出所有值,填回界面。存储7个数据:input_modegateway_urlappstreamprotocoldirect_urltheme_mode

09 · 日志系统

📖 什么是日志?为什么需要它?

日志就是程序在运行过程中写的流水账——"14:32:05 用户点击了播放"、"14:32:07 连接失败,原因是网络不通"。核心作用是事后排查——出了问题回头看日志就知道"到底发生了什么、在哪一步出的事"。没有日志只能瞎猜。飞机上的"黑匣子"就是飞行日志——出事后调查员靠它还原事发经过。

日志面板位于 App 底部。每条日志含:时间戳、级别(INFO/WARN/ERROR)、消息内容。三个来源:用户操作、播放器状态变化、错误事件。管理:上限200条、自动滚动、一键复制、一键清空、实时级别统计。

10 · 播放器控件设计

📖 点播 vs 直播——为什么影响控件设计?

点播(VOD):视频是一个已经录制好的完整文件,有明确的总时长(比如10分30秒),你可以拖进度条跳到第5分钟——因为第5分钟的内容已经存在了。直播(Live Streaming):画面正在实时产生。第5分钟的内容现在还不存在(还没发生),你当然没法跳过去。本项目是直播——进度条完全没用,只会误导用户。所以关了进度条、拖拽手势、双击快进。

📼 点播(VOD)—— 录好的文件 0:00 全部内容已存在(已录制完成) 10:30 可跳到任意位置 (第5分钟的内容已经存在) ◀━━━ ━━━▶ App:显示进度条 ✅ 允许拖拽 ✅ 🔴 直播(Live)—— 实时产生 0:00 已发生的(可以回看但意义不大) ← 当前时刻 未来的(还没发生,不存在) 进度条无用——不能跳到"还没发生"的时间点 App:隐藏进度条 ❌ 禁拖拽 ❌ 禁双击快进 ❌
▲ 点播 vs 直播的本质区别:点播的全部内容都已存在,直播的未来部分还不存在。这决定了控件要不要进度条
平台按钮为什么
Windows 桌面播放/暂停 + 音量 + 全屏屏幕大,鼠标精确,多放几个不拥挤
Android 手机仅全屏按钮屏幕小,手指粗,多了容易误触

11 · 错误处理

项目覆盖了7种可能出错的场景——每种都在出问题之前检查,或出问题之后捕获,给用户清晰的提示。

什么错了怎么发现的告诉用户什么
填的信息不完整发请求前检查"请填写服务地址、App、Stream"
网址为空播放前检查"请输入播放地址"
Service 返回错误(如404)看 HTTP 状态码"gateway 响应 404: ..."
Service 回了乱码解析时验证格式"响应格式无效"
想要的协议没有地址检查字段是否空"响应中缺少 playHttpFlv"
Web 环境用了 FLV/RTMP检测网址特征"Web环境不支持,请切换 HLS"
播放引擎启动失败try-catch 抓异常"播放失败: 具体原因"

12 · 跨平台策略

📖 什么是"平台"?为什么跨平台是件大事?

一部 Android 手机和一台 Windows 电脑,底层是完全不同的操作系统——管理硬件的最底层软件。不同操作系统有不同的"规矩":Android 用 Linux 内核+Kotlin,Windows 用 NT 内核+C++,浏览器用 JavaScript 引擎。传统做法是一个 App 写三遍,Flutter 的做法是写一遍 Dart,编译工具分别翻译成各平台的原生机器指令——这就是跨平台的核心价值。

平台支持度怎么编译的限制
WindowsDart→原生x64 + C++用CMake
AndroidDart→ARM64 + Gradle打包需允许明文HTTP
Web⚠️ 待配置代码已预留(kIsWeb 分支),尚未 flutter create web仅支持 HLS
iOS未配置iOS编译
Android 明文 HTTP 配置:2018年 Android 9 开始要求 App 默认用 HTTPS 加密,HTTP 明文被拦截——防范公共 WiFi 下数据被窃取。但本项目是局域网内部使用,流媒体服务没有 HTTPS 证书。所以配置文件中声明 android:usesCleartextTraffic="true"——仅本 App 豁免,不影响其他 App。

13 · 构建与发布

📖 什么是"构建"?源代码怎么变成能装的 App?

程序员写的代码(Dart、C++、Kotlin)是人类可读的文本。但手机芯片只懂机器码(0和1组成的硬件指令)。构建就是通过编译器把人类可读代码翻译成机器码。Flutter 的构建分两步:先编译 Dart → 各平台原生机器指令,再调用各平台打包工具压成安装包。

📝 源代码 Dart / C++ / Kotlin 人类可读文本 ~1400行 ⚙️ 构建工具链 编译器 + 打包器 一条命令自动完成 Windows .zip 桌面安装包 Android .apk 侧载安装包 Android .aab Play商店上架 checksums.txt SHA256 数字指纹 release-notes-template.md 发布说明模板 ↑ 一条命令 → 5个产物,集中输出到 dist/ 目录
▲ 构建流水线:源代码 → 编译+打包 → 5个产物自动输出
📖 APK vs AAB 的区别

APK:传统格式,包含所有资源,能直接传到手机上安装(侧载),不需要商店。坏处是包很大——包含了用不上的资源。AAB:2018年 Google 推出,是一个"零件清单"——从 Play 商店下载时,Google 根据你的具体手机型号动态组装最优包。AAB 不能直接安装,必须通过商店。

📖 SHA256 校验是什么?

哈希算法:一种数学函数——输入任意数据,输出固定长度的"指纹"。同一文件指纹始终相同;文件哪怕只改一个字节,指纹就完全不同。SHA256 是一种广泛使用的哈希算法,输出64个十六进制字符。发布软件时附带 SHA256 指纹,下载者可验证文件是否完好无损。

# 一键构建 Windows + Android 发布包 powershell -ExecutionPolicy Bypass -File .\scripts\build_release.ps1 # 产物输出到 dist/ 目录: # fplayer-ff-mobile-windows.zip → Windows 桌面版 # fplayer-ff-mobile-android.apk → Android 侧载安装包 # fplayer-ff-mobile-android.aab → Google Play 上架包 # checksums.txt → SHA256 校验文件

Android 签名

Android 要求安装包必须经过数字签名——用开发者的私钥加密标记,证明"这个包确实是我做的,中途没被掉包"。有正式密钥则正式签名(能上架商店);没有则用 Debug 签名(仅内部测试,不能上架)。

14 · 权限与安全

📖 Android 权限系统——为什么 App 要"申请"才能联网?

Android 设计了权限机制:每个 App 安装前必须声明自己要用哪些敏感功能(联网、摄像头、定位……)。用户在安装时就能看到并判断是否合理。INTERNET 权限是最基础的——允许 App 发起网络连接。在配置文件中写 <uses-permission android:name="android.permission.INTERNET"/>

📖 NAT(网络地址转换)——为什么外面看不到你家里设备?

路由器给每台设备分一个私有 IP(如 192.168.x.x),只在局域网内有效——出了路由器没人认识。路由器对外只暴露一个公网 IP。这个机制叫 NAT,天然形成安全隔离——外部攻击者无法主动连接你局域网里的设备,因为不知道地址。本项目的所有视频数据都在局域网内传输,外部接触不到。

🏠 你的局域网(私有 IP 段 192.168.x.x) 🖥️ Desktop 192.168.1.5 ⚙️ Service 192.168.1.6 📱 Mobile 192.168.1.8 视频数据在 局域网内 直接传输 路由器(NAT:私有 IP ↔ 公网 IP 转换)公网 IP:203.0.113.5 🌐 互联网(公网) 👤 外部攻击者 无法访问 192.168.x.x 是私有地址 ✅ 安全:外部看不到内网设备 只能看到路由器的公网IP 但不知道后面有什么设备
▲ NAT 安全隔离:局域网内的私有 IP 对外部完全不可见。攻击者只能看到路由器的公网 IP,无法主动连接内网设备

如果需要远程观看怎么办?

不要把流媒体端口直接暴露到公网——全世界都能扫到。安全做法是用 VPN(虚拟专用网络)——在公网上建立一条加密隧道,让远程设备"虚拟地"接入你的局域网,获得局域网内的虚拟 IP,就可以像在本地一样访问所有内网资源。