mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
feat(playback): client direct-play decode mode (release host transcoding)
Add an admin toggle playback.direct_only that offloads all decoding to the client. When enabled: - Emby PlaybackInfo no longer advertises SupportsTranscoding nor a TranscodingUrl, forcing Emby/Infuse/Yamby clients to direct play. - HLS endpoints refuse (ErrTranscodeDisabled) so the host never spawns ffmpeg. - Web player forces direct play, hides the HLS toggle, and surfaces a hint. Exposed via /system/info (direct_play_only) and the Settings page. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -13,6 +13,7 @@ export const systemAPI = {
|
||||
arch: string
|
||||
data_dir: string
|
||||
cache_dir: string
|
||||
direct_play_only?: boolean
|
||||
}>('/system/info')
|
||||
.then((r) => r.data),
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { mediaAPI } from '../api/library'
|
||||
import { api, hlsURL, streamURL } from '../api/client'
|
||||
import { playbackAPI } from '../api/playback'
|
||||
import { subtitlesAPI, type SubtitleTrack } from '../api/subtitles'
|
||||
import { systemAPI } from '../api/system'
|
||||
import type { Media } from '../types'
|
||||
|
||||
type Mode = 'direct' | 'hls'
|
||||
@@ -37,6 +38,16 @@ export function PlayerPage() {
|
||||
const [subs, setSubs] = useState<SubtitleTrack[]>([])
|
||||
const [hlsUnavailable, setHlsUnavailable] = useState(false)
|
||||
const [playerError, setPlayerError] = useState('')
|
||||
// 「客户端直连解码」模式:宿主机不转码,播放器强制 direct play、隐藏 HLS 切换。
|
||||
const [directOnly, setDirectOnly] = useState(false)
|
||||
|
||||
// 读取宿主机的「直连解码」开关。开启时全程 direct play,不走 HLS。
|
||||
useEffect(() => {
|
||||
systemAPI
|
||||
.info()
|
||||
.then((info) => setDirectOnly(Boolean(info.direct_play_only)))
|
||||
.catch(() => setDirectOnly(false))
|
||||
}, [])
|
||||
|
||||
// Load metadata and pick a default mode.
|
||||
useEffect(() => {
|
||||
@@ -45,14 +56,15 @@ export function PlayerPage() {
|
||||
setMedia(m)
|
||||
const forced = params.get('mode') as Mode | null
|
||||
const auto = pickMode(m)
|
||||
setMode(forced ?? auto)
|
||||
// 直连解码模式下忽略 ?mode=hls 与自动判定,始终 direct play。
|
||||
setMode(directOnly ? 'direct' : (forced ?? auto))
|
||||
setPlayerError('')
|
||||
})
|
||||
subtitlesAPI
|
||||
.list(id)
|
||||
.then((tracks) => setSubs(tracks ?? []))
|
||||
.catch(() => setSubs([]))
|
||||
}, [id, params])
|
||||
}, [id, params, directOnly])
|
||||
|
||||
// Wire up the actual <video> element when we know the mode.
|
||||
useEffect(() => {
|
||||
@@ -161,26 +173,35 @@ export function PlayerPage() {
|
||||
<ArrowLeft size={16} /> 返回
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
const next = mode === 'hls' ? 'direct' : 'hls'
|
||||
setMode(next)
|
||||
params.set('mode', next)
|
||||
setParams(params, { replace: true })
|
||||
}}
|
||||
className="pointer-events-auto flex items-center gap-2 rounded-full border border-white/15 bg-black/70 px-4 py-2 text-sm font-medium text-white shadow-xl backdrop-blur transition hover:bg-black/85"
|
||||
title="切换播放模式"
|
||||
>
|
||||
{mode === 'hls' ? (
|
||||
<>
|
||||
<RefreshCw size={14} /> HLS 转码中
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles size={14} /> 直接播放
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{directOnly ? (
|
||||
<span
|
||||
className="pointer-events-auto flex items-center gap-2 rounded-full border border-white/15 bg-black/70 px-4 py-2 text-sm font-medium text-white shadow-xl backdrop-blur"
|
||||
title="宿主机不转码,由客户端本地解码直连"
|
||||
>
|
||||
<Sparkles size={14} /> 客户端直连解码
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
const next = mode === 'hls' ? 'direct' : 'hls'
|
||||
setMode(next)
|
||||
params.set('mode', next)
|
||||
setParams(params, { replace: true })
|
||||
}}
|
||||
className="pointer-events-auto flex items-center gap-2 rounded-full border border-white/15 bg-black/70 px-4 py-2 text-sm font-medium text-white shadow-xl backdrop-blur transition hover:bg-black/85"
|
||||
title="切换播放模式"
|
||||
>
|
||||
{mode === 'hls' ? (
|
||||
<>
|
||||
<RefreshCw size={14} /> HLS 转码中
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles size={14} /> 直接播放
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
@@ -195,7 +216,10 @@ export function PlayerPage() {
|
||||
// 浏览器对 <video src> 的错误描述非常有限,把详细原因
|
||||
// 转给开发者控制台 + 一条 toast;常见原因是 codec 不支持。
|
||||
if (mode === 'direct') {
|
||||
if (hlsUnavailable) {
|
||||
if (directOnly) {
|
||||
setPlayerError('直接播放失败。当前为「客户端直连解码」模式,宿主机不转码;请使用支持该编码/封装的播放器(如 Infuse / VLC / Emby 客户端)播放,或关闭直连解码模式。')
|
||||
toast.error('直接播放失败(客户端直连解码模式)')
|
||||
} else if (hlsUnavailable) {
|
||||
setPlayerError('直接播放失败,且 HLS 转码不可用。请检查文件是否存在,或配置本机 ffmpeg 后使用 HLS 转码播放。')
|
||||
toast.error('直接播放失败,HLS 转码不可用')
|
||||
} else {
|
||||
|
||||
@@ -47,11 +47,18 @@ const GROUPS: SettingGroup[] = [
|
||||
{ value: 'ja-JP', label: '日本語' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'playback.direct_only',
|
||||
label: '客户端直连解码(释放宿主机资源)',
|
||||
type: 'toggle',
|
||||
hint: '默认关闭。开启后宿主机不再进行任何 FFmpeg 转码,所有播放交给第三方客户端(Infuse / VLC / Emby 客户端等)或浏览器本地解码直连(direct play / 302 直链),大幅降低宿主机 CPU 占用。若客户端不支持源编码可能无法播放。',
|
||||
defaultValue: 'false',
|
||||
},
|
||||
{
|
||||
key: 'transcode.enabled',
|
||||
label: '启用转码',
|
||||
type: 'toggle',
|
||||
hint: '关闭后所有视频直连播放',
|
||||
hint: '关闭后所有视频直连播放(「客户端直连解码」开启时本项自动失效)',
|
||||
defaultValue: 'true',
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user