Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions docs/en/guide/web-player.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,6 @@ The player page path can be customized via the `player-page-path` configuration
> [!IMPORTANT]
> The player relies on the browser's native decoding capabilities. Some encoding formats (such as E-AC3) may not play in certain browsers (manifested as no audio or black screen). We recommend using the latest versions of Chrome, Edge, or Safari.

## MP2 Audio Software Decoding

Most IPTV HD and SD channels use MPEG-1 Layer 2 (MP2) audio encoding. Some browsers (such as iOS Safari) do not natively support MP2 decoding, causing programs to fail to play or play with video only (no audio).

The player has built-in MP2 audio software decoding capability:

- **iOS Safari**: Enabled by default. Testing shows most programs can now play normally.
- **Other browsers**: Disabled by default. You can manually enable the "MP2 Audio Software Decoding" option by clicking the sidebar settings button.

> [!NOTE]
> Audio software decoding relies on browser Web Workers and WebAssembly for background decoding, which consumes some computational resources and may cause slight heating on mobile devices — this is normal. Additionally, due to browser limitations, background playback on mobile devices is not supported when using software decoding.

## PWA Support and Add to Home Screen

The built-in web player supports PWA (Progressive Web App). You can add the player page to your device's home screen (including phones, tablets, computers, and LG webOS smart TVs) and launch it like a native app for a full-screen, immersive viewing experience.
Expand Down
12 changes: 0 additions & 12 deletions docs/guide/web-player.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,6 @@ http://192.168.1.1:5140/player
> [!IMPORTANT]
> 播放器依赖浏览器的原生解码能力,部分编码格式(如 E-AC3)可能在某些浏览器中无法播放(表现为无音频、画面黑屏)。推荐使用最新版本的 Chrome、Edge 或 Safari。

## MP2 音频软解

IPTV 大多数高清、标清频道使用 MPEG-1 Layer 2 (MP2) 音频编码,一些浏览器(例如 iOS Safari)无法原生支持 MP2 解码,导致节目无法播放,或者只有画面没有音频。

播放器内置了 MP2 音频软解能力:

- **iOS Safari**:默认启用,经测试大多数节目已经可以正常播放
- **其他浏览器**:默认关闭,可以点击侧边栏设置按钮手动启用「MP2 音频软解」选项

> [!NOTE]
> 音频软解依赖于浏览器 Web Worker 和 WebAssembly 在后台解码,会占用一些计算资源,在手机上可能会产生轻微发热,这是正常现象。此外,受限于浏览器,使用软解时在手机上无法保持后台播放。

## PWA 支持与添加到主屏幕

内置 Web 播放器支持 PWA(Progressive Web App)。你可以将播放器页面添加到设备主屏幕(包括手机、平板、电脑,以及 LG webOS 智能电视),像打开原生应用一样一键进入,获得全屏、沉浸式的观看体验。
Expand Down
10 changes: 0 additions & 10 deletions web-ui/src/components/player/settings-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ interface SettingsDropdownProps {
onThemeChange: (theme: ThemeMode) => void;
seamlessSwitch: boolean;
onSeamlessSwitchChange: (enabled: boolean) => void;
mp2SoftDecode: boolean;
onMp2SoftDecodeChange: (enabled: boolean) => void;
}

const localeOptions: Array<{ value: Locale; label: string }> = [
Expand All @@ -38,8 +36,6 @@ function SettingsDropdownComponent({
onThemeChange,
seamlessSwitch,
onSeamlessSwitchChange,
mp2SoftDecode,
onMp2SoftDecodeChange,
}: SettingsDropdownProps) {
const t = usePlayerTranslation(locale);
const [isOpen, setIsOpen] = useState(false);
Expand Down Expand Up @@ -113,12 +109,6 @@ function SettingsDropdownComponent({
aria-label={t("seamlessSwitch")}
/>
</div>

{/* MP2 Audio Software Decode Toggle */}
<div className="flex items-center justify-between px-1">
<span className="text-xs font-medium text-muted-foreground">{t("mp2SoftDecode")}</span>
<Switch checked={mp2SoftDecode} onCheckedChange={onMp2SoftDecodeChange} aria-label={t("mp2SoftDecode")} />
</div>
</div>
</div>
)}
Expand Down
47 changes: 6 additions & 41 deletions web-ui/src/components/player/video-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ interface VideoPlayerProps {
onToggleSidebar?: () => void;
onFullscreenToggle?: () => void;
seamlessSwitch?: boolean;
mp2SoftDecode?: boolean;
activeSourceIndex?: number;
onSourceChange?: (index: number) => void;
onPlaybackStarted?: () => void;
Expand Down Expand Up @@ -137,7 +136,6 @@ export function VideoPlayer({
onToggleSidebar,
onFullscreenToggle,
seamlessSwitch = true,
mp2SoftDecode = false,
activeSourceIndex = 0,
onSourceChange,
onPlaybackStarted,
Expand Down Expand Up @@ -541,7 +539,7 @@ export function VideoPlayer({
setNeedsUserInteraction(true);
});

const createPlayerForSlot = useEffectEvent((slotId: SlotId, useMp2SoftDecode = mp2SoftDecode): Player | null => {
const createPlayerForSlot = useEffectEvent((slotId: SlotId): Player | null => {
const video = slotVideoRef(slotId).current;
if (!video || !isSupported()) return null;

Expand All @@ -552,7 +550,7 @@ export function VideoPlayer({
video.muted = isMuted;

const p = createPlayer(video, {
wasmDecoders: useMp2SoftDecode ? { mp2: mp2WasmUrl } : {},
wasmDecoders: { mp2: mp2WasmUrl },
});
p.on("error", (e) => {
if (slotPlayerRef(slotId).current === p) {
Expand Down Expand Up @@ -650,9 +648,11 @@ export function VideoPlayer({

// Load segments whenever they change (channel/source switch, seek, retry — all go through here)
const handleLoadSegments = useEffectEvent((newSegments: PlayerSegment[]) => {
if (!newSegments.length) return;

const activeId = getActiveSlotId();
const activePlayer = slotPlayerRef(activeId).current;
if (!newSegments.length || !activePlayer) return;
const activePlayer = slotPlayerRef(activeId).current ?? createPlayerForSlot(activeId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Create the initial player before loading segments

On a fresh /player mount, the segments effect below still returns when the active slot has no player, but this commit removed the mount-time decoder effect that used to create slot A. That means the first non-empty segments update after loading a playlist never calls handleLoadSegments, so this new lazy createPlayerForSlot fallback is never reached and no MediaSource/player is created until some unrelated path creates one.

Useful? React with 👍 / 👎.

if (!activePlayer) return;

console.log("Loading segments...");

Expand Down Expand Up @@ -723,10 +723,6 @@ export function VideoPlayer({
handleLoadSegments(newSegments);
});

const reloadAfterDecoderChange = useEffectEvent(() => {
handleLoadSegments(segments);
});

useEffect(() => {
return () => {
cancelPendingTransition();
Expand All @@ -735,37 +731,6 @@ export function VideoPlayer({
};
}, []);

// Recreate decoder pipeline when mp2SoftDecode toggles
useEffect(() => {
if (!slotAVideoRef.current || !isSupported()) return;
const hadPlayer = slotAPlayerRef.current !== null || slotBPlayerRef.current !== null;
const activeVideo = (activeSlotIdRef.current === "a" ? slotAVideoRef : slotBVideoRef).current;
const shouldResumeAfterRecreate = activeVideo
? !activeVideo.paused && !activeVideo.ended
: shouldAutoPlayRef.current;

cancelPendingTransition();
transitionGenRef.current++;
if (hadPlayer) {
shouldAutoPlayRef.current = shouldResumeAfterRecreate;
userPausedRef.current = !shouldResumeAfterRecreate;
setNeedsUserInteraction(false);
}
wallClockCalibratedRef.current = false;
setLiveSessionAnchor(null);
destroySlot("a");
destroySlot("b");
hasStartedPlaybackRef.current = false;
activeSlotIdRef.current = "a";
setVisibleSlotId("a");

createPlayerForSlot("a", mp2SoftDecode);

if (hadPlayer) {
reloadAfterDecoderChange();
}
}, [mp2SoftDecode]);

useEffect(() => {
if (!seamlessSwitch) {
stopPendingTransition();
Expand Down
3 changes: 0 additions & 3 deletions web-ui/src/i18n/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ const base: TranslationDict = {
themeLight: "Light",
themeDark: "Dark",
seamlessSwitch: "Seamless switch",
mp2SoftDecode: "MP2 Audio Software Decoder",
};

const zhHans: TranslationDict = {
Expand Down Expand Up @@ -197,7 +196,6 @@ const zhHans: TranslationDict = {
themeLight: "浅色",
themeDark: "深色",
seamlessSwitch: "无缝换台",
mp2SoftDecode: "MP2 音频软解",
};

// 繁體中文(偏好香港用語)
Expand Down Expand Up @@ -297,7 +295,6 @@ const zhHant: TranslationDict = {
themeLight: "淺色",
themeDark: "深色",
seamlessSwitch: "無縫換台",
mp2SoftDecode: "MP2 音頻軟解",
};

export const translations: Record<Locale, TranslationDict> = {
Expand Down
3 changes: 0 additions & 3 deletions web-ui/src/lib/player-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
* JSON serialization, error handling, and backward-compatible reads.
*/

import { isIOS } from "./platform";

function cloneDefaultValue<T>(value: T): T {
if (value === null || typeof value !== "object") {
return value;
Expand Down Expand Up @@ -45,7 +43,6 @@ export const [getLastChannelId, saveLastChannelId] = createStore<string | null>(
);
export const [getSidebarVisible, saveSidebarVisible] = createStore("rtp2httpd-player-sidebar-visible", true);
export const [getSeamlessSwitch, saveSeamlessSwitch] = createStore("rtp2httpd-player-seamless-switch", true);
export const [getMp2SoftDecode, saveMp2SoftDecode] = createStore("rtp2httpd-player-mp2-soft-decode", isIOS());
export const [getVolume, saveVolume] = createStore("rtp2httpd-player-volume", 1);
export const [getMuted, saveMuted] = createStore("rtp2httpd-player-muted", false);

Expand Down
22 changes: 1 addition & 21 deletions web-ui/src/pages/player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ import { buildCatchupSegments, clampCatchupStartTime, parseM3U } from "../lib/m3
import {
getLastChannelId,
getLastSourceIndex,
getMp2SoftDecode,
getSeamlessSwitch,
getSidebarVisible,
saveLastChannelId,
saveLastSourceIndex,
saveMp2SoftDecode,
saveSeamlessSwitch,
saveSidebarVisible,
} from "../lib/player-storage";
Expand Down Expand Up @@ -57,7 +55,6 @@ function PlayerPage() {
const [isFullscreen, setIsFullscreen] = useState(false);
const [isMobile, setIsMobile] = useState(() => window.innerWidth < 768);
const [seamlessSwitch, setSeamlessSwitch] = useState(() => getSeamlessSwitch());
const [mp2SoftDecode, setMp2SoftDecode] = useState(() => getMp2SoftDecode());
const pageContainerRef = useRef<HTMLDivElement>(null);

// Track stream start time - the absolute time position when current stream started
Expand Down Expand Up @@ -340,11 +337,6 @@ function PlayerPage() {
saveSeamlessSwitch(enabled);
}, []);

const handleMp2SoftDecodeChange = useCallback((enabled: boolean) => {
setMp2SoftDecode(enabled);
saveMp2SoftDecode(enabled);
}, []);

const handleToggleSidebar = useCallback(() => {
setShowSidebar((prev) => {
const newState = !prev;
Expand All @@ -363,21 +355,10 @@ function PlayerPage() {
onThemeChange={setTheme}
seamlessSwitch={seamlessSwitch}
onSeamlessSwitchChange={handleSeamlessSwitchChange}
mp2SoftDecode={mp2SoftDecode}
onMp2SoftDecodeChange={handleMp2SoftDecodeChange}
/>
</div>
);
}, [
locale,
theme,
seamlessSwitch,
mp2SoftDecode,
setLocale,
setTheme,
handleSeamlessSwitchChange,
handleMp2SoftDecodeChange,
]);
}, [locale, theme, seamlessSwitch, setLocale, setTheme, handleSeamlessSwitchChange]);

// Main UI content
const mainContent = (
Expand Down Expand Up @@ -405,7 +386,6 @@ function PlayerPage() {
onToggleSidebar={handleToggleSidebar}
onFullscreenToggle={handleFullscreenToggle}
seamlessSwitch={seamlessSwitch}
mp2SoftDecode={mp2SoftDecode}
activeSourceIndex={activeSourceIndex}
onSourceChange={handleSourceChange}
onPlaybackStarted={handlePlaybackStarted}
Expand Down
Loading