视频:修 media.php 的 304/Range 冲突,HEVC 解不了时给明确提示 (#52)
304 判定没有排除 Range 请求:视频第二次打开时浏览器同时发 Range + If-None-Match,命中 304 拿到空响应 ⇒ 黑屏。 改成 $hasRange 为真时跳过 304。顺手补 Last-Modified(部分浏览器需要它才允许 seek)。 另查明用户的视频是 H.265/HEVC,Chrome/Firefox 无法解码 —— 浏览器端无解, 检测到解码失败时显示提示卡片 + 下载入口,不再给一块黑屏。
This commit is contained in:
+171
-3
@@ -2412,7 +2412,19 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb,
|
||||
max-width: 92vw; max-height: 100%; width: auto; height: auto;
|
||||
object-fit: contain; border-radius: 8px;
|
||||
}
|
||||
.pv-video { max-width: 92vw; max-height: 100%; width: auto; height: auto; background: #000; }
|
||||
/* 🔴 2026-11-05 修:视频打开后是黑的(用户:「点开之后是黑的,没办法播放」)。
|
||||
原因:`max-height: 100%` 依赖父级的**确定高度**,而 `.pv-stage` 是
|
||||
flex + `align-items:center`,百分比高度解析不出具体像素
|
||||
⇒ `<video>` 按 0 高度渲染 ⇒ 只剩 background:#000 一块黑。
|
||||
改成 `vh`(相对视口,永远是确定值)+ `object-fit: contain`(竖屏不变形)。 */
|
||||
.pv-video {
|
||||
max-width: 92vw;
|
||||
max-height: 82vh;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
.pv-pdf {
|
||||
width: 92vw; height: 100%; border: 0; border-radius: 8px; background: #fff;
|
||||
}
|
||||
@@ -3949,8 +3961,56 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb,
|
||||
font-size: 11.5px; padding: 1px 8px; border-radius: 5px;
|
||||
background: var(--line-2); color: var(--muted);
|
||||
}
|
||||
.node-route-line { font-size: 12.5px; color: var(--muted); }
|
||||
.node-route-line .arrow { color: var(--faint); margin: 0 3px; }
|
||||
/* 🔴 2026-11-05 重做(用户:「这个位都没对齐呀…直接用个标签,然后右边一个箭头,
|
||||
到北京南站就得了,不用写从哪到哪」)。
|
||||
|
||||
原来只是一个普通 div,里面的 <a> 与 <span class="arrow"> 各按自己的基线排
|
||||
⇒ 字号不同就"没对齐"。
|
||||
|
||||
现在:容器 flex + align-items:center,两个地名各自成胶囊标签,
|
||||
箭头只作窄分隔。视觉上就是「[青岛北站] → [北京南站]」,
|
||||
不再需要"从/到"这种字。 */
|
||||
.node-route-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
font-size: 12.5px;
|
||||
color: var(--muted);
|
||||
line-height: 1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 地名标签:浅底圆角,长名字省略 */
|
||||
.node-route-line > a,
|
||||
.node-route-line > span.route-stop {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 46%;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--fill, #F1EAE1);
|
||||
color: var(--ink, #2C231D);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.node-route-line > a:hover {
|
||||
background: var(--brand-soft, #F5E3DC);
|
||||
}
|
||||
|
||||
/* 箭头:窄、淡,只做分隔 */
|
||||
.node-route-line .arrow {
|
||||
color: var(--faint);
|
||||
margin: 0 1px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
flex: none;
|
||||
}
|
||||
.node-spent { font-size: 12.5px; color: var(--brand-ink); }
|
||||
.node-member { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.node-role {
|
||||
@@ -7004,3 +7064,111 @@ label.is-req,
|
||||
.dark .mv-row.is-req > .mv-lb {
|
||||
color: #F08A78 !important;
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
#47(2026-11-05)行程状态动作按钮
|
||||
------------------------------------------------------------
|
||||
用户:「做成两个按钮,颜色也尽可能保持一致…
|
||||
选中其中一个按钮之后…还加了一个那种椭圆的那种框那种背景,灰色的。」
|
||||
|
||||
实现:胶囊(border-radius:999px)+ 浅底;
|
||||
`.is-on` 是"当前状态",用实心主题色表示"正在被选中",
|
||||
其余动作是浅底 —— 一眼能看出现在是什么、能改成什么。
|
||||
============================================================ */
|
||||
.node-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 4px 0 2px;
|
||||
}
|
||||
|
||||
.node-act {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
background: var(--fill, #F1EAE1);
|
||||
color: var(--muted, #8A7A6D);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background .12s ease, color .12s ease;
|
||||
}
|
||||
|
||||
.node-act:hover { background: var(--brand-soft, #F5E3DC); }
|
||||
|
||||
/* 当前状态:实心 = 正在被选中 */
|
||||
.node-act.is-on {
|
||||
background: var(--muted, #8A7A6D);
|
||||
color: #fff;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.node-act-go { color: #2F6FA8; }
|
||||
.node-act-go:hover { background: #EAF2F9; }
|
||||
|
||||
.node-act-ok { color: #3F7D5A; }
|
||||
.node-act-ok:hover { background: #EAF4EE; }
|
||||
|
||||
.node-act-no { color: #C0533F; }
|
||||
.node-act-no:hover { background: #FBEDEA; }
|
||||
|
||||
.node-act-gray { color: var(--muted, #8A7A6D); }
|
||||
|
||||
|
||||
/* #48(2026-11-05)补充:取消按钮(琥珀色,与"删除"的砖红区分开)。
|
||||
用户强调「取消和删除是不一样的」⇒ 视觉上也不能同色。 */
|
||||
.node-act-warn { color: #B8862B; }
|
||||
.node-act-warn:hover { background: #FDF6E7; }
|
||||
|
||||
/* 时间行:计划与实际并列时的区分 */
|
||||
.node-time .t-plan { color: var(--muted, #8A7A6D); }
|
||||
.node-time .t-actual {
|
||||
color: var(--brand, #A8574F);
|
||||
margin-left: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
#53(2026-11-05)视频解不了时的提示卡片
|
||||
------------------------------------------------------------
|
||||
用户传的视频是 H.265/HEVC,Chrome/Firefox 解不了 ⇒ 黑屏。
|
||||
不给提示的话用户会以为"文件坏了/网站坏了"。
|
||||
这里把原因和出路直接写在画面里。
|
||||
============================================================ */
|
||||
.pv-fallback {
|
||||
max-width: 520px;
|
||||
padding: 28px 24px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, .08);
|
||||
border: 1px solid rgba(255, 255, 255, .18);
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
.pv-fallback-h {
|
||||
margin: 0 0 10px;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.pv-fallback-p {
|
||||
margin: 0 0 18px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
opacity: .82;
|
||||
}
|
||||
.pv-fallback-dl {
|
||||
display: inline-block;
|
||||
padding: 9px 22px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #1a1a1a;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
.pv-fallback-dl:hover { background: #eee; }
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
var kind = trigger.getAttribute('data-prev') || 'image';
|
||||
var src = trigger.getAttribute('data-src') || trigger.getAttribute('href') || '';
|
||||
var dl = trigger.getAttribute('data-dl') || src;
|
||||
/* #53:文件名要在 error 回调里用到,提前取出来(原来 nm 在函数末尾才定义) */
|
||||
var nm2 = trigger.getAttribute('data-name') || '';
|
||||
if (!src) { return false; }
|
||||
opener = trigger;
|
||||
clear();
|
||||
@@ -82,6 +84,37 @@
|
||||
el.controls = true;
|
||||
el.autoplay = true;
|
||||
el.playsInline = true;
|
||||
/* 🔴🔴 #53(2026-11-05):**HEVC 解码失败时给明确提示,不要装死**。
|
||||
实测用户的视频是 H.265/HEVC(`hvc1`),Chrome/Firefox 的根本解不了 ——
|
||||
表现为一块黑屏,什么提示都没有,用户只会以为"文件坏了"。
|
||||
|
||||
这里监听 error:能取到 `el.error.code` 就说明是解码/加载失败。
|
||||
把解释写进画面本身(而不是 alert),并给出下载入口 ——
|
||||
下载下来用系统播放器是能看的(VLC/iPhone/Windows 都支持 HEVC)。
|
||||
|
||||
⚠️ 不做"静默回退到下载页":用户点的是"看",直接跳走会困惑。
|
||||
要的是**在原地说明白**,同时给一条出路。 */
|
||||
el.addEventListener('error', function () {
|
||||
var wrap = document.createElement('div');
|
||||
wrap.className = 'pv-fallback';
|
||||
var h = document.createElement('p');
|
||||
h.className = 'pv-fallback-h';
|
||||
h.textContent = '这个视频浏览器解不了';
|
||||
var p = document.createElement('p');
|
||||
p.className = 'pv-fallback-p';
|
||||
p.textContent = '它可能是 H.265(HEVC)编码。Chrome、Firefox 不支持这种格式,'
|
||||
+ '但文件本身没坏 —— 下载后用系统播放器(iPhone / VLC / Windows 播放器)可以正常观看。';
|
||||
var a = document.createElement('a');
|
||||
a.className = 'pv-fallback-dl';
|
||||
a.href = dl;
|
||||
a.textContent = '下载这个视频';
|
||||
if (nm2) { a.setAttribute('download', nm2); }
|
||||
wrap.appendChild(h);
|
||||
wrap.appendChild(p);
|
||||
wrap.appendChild(a);
|
||||
while (stage.firstChild) { stage.removeChild(stage.firstChild); }
|
||||
stage.appendChild(wrap);
|
||||
});
|
||||
} else if (kind === 'pdf') {
|
||||
el = document.createElement('iframe');
|
||||
el.className = 'pv-pdf';
|
||||
|
||||
@@ -217,8 +217,25 @@ $etag = '"' . md5($key . '|' . $size . '|' . $mime) . '"';
|
||||
v47 的缓存回写(见第 3 步)让第二次开始命中的是**本地文件**,
|
||||
所以这里"取回来再判 304"的代价已经降到本地读盘级别,够用了。
|
||||
(真要连本地读都省掉,得把 ETag 单独持久化 —— 那是下一步的事。) */
|
||||
|
||||
/* 🔴🔴 2026-11-05 修(用户:「网页端的视频…点开之后是黑的,没办法播放」):
|
||||
**带 Range 的请求绝不能回 304**。
|
||||
|
||||
原来的顺序是"先判 304,再处理 Range"。视频第一次播完后浏览器缓存了 ETag,
|
||||
拖动进度条 / 第二次打开时会同时发:
|
||||
|
||||
Range: bytes=1048576-
|
||||
If-None-Match: "abc123"
|
||||
|
||||
⇒ 命中 304(304 **没有响应体**)⇒ `<video>` 拿不到数据 ⇒ **黑屏**。
|
||||
这就是"有时候能播、有时候黑"的根因:取决于浏览器有没有带 ETag。
|
||||
|
||||
RFC 7233 也明确:304 不能替代对 Range 的响应。
|
||||
⇒ 有 Range 就跳过 304,直接走第 6 步。 */
|
||||
$hasRange = trim((string)($_SERVER['HTTP_RANGE'] ?? '')) !== '';
|
||||
|
||||
$inm = trim((string)($_SERVER['HTTP_IF_NONE_MATCH'] ?? ''));
|
||||
if ($inm !== '' && $inm === $etag) {
|
||||
if (!$hasRange && $inm !== '' && $inm === $etag) {
|
||||
http_response_code(304);
|
||||
header('ETag: ' . $etag);
|
||||
header('Cache-Control: private, max-age=604800');
|
||||
@@ -261,6 +278,9 @@ if ($isRange) {
|
||||
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size);
|
||||
}
|
||||
header('Accept-Ranges: bytes');
|
||||
/* 2026-11-05:补 Last-Modified —— 部分浏览器/Safari 需要它才允许 seek,
|
||||
缺了会对某些 mp4 直接拒绝播放(表现为黑屏 + 无报错)。 */
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
|
||||
$filename = basename($key);
|
||||
$disp = !empty($_GET['dl']) ? 'attachment' : 'inline';
|
||||
header('Content-Disposition: ' . $disp . '; filename="' . rawurlencode($filename) . '"');
|
||||
|
||||
Reference in New Issue
Block a user