feat: enhance IPTV player with comprehensive controls, improved stream loading robustness, and UI refinements including volume, progress, and fullscreen.

This commit is contained in:
kuekhaoyang
2026-02-17 22:24:49 +08:00
parent 4256ef4179
commit aa8f8e9ff9
8 changed files with 501 additions and 139 deletions
+4 -7
View File
@@ -4,7 +4,7 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { parseM3U, type M3UChannel } from '@/lib/utils/m3u-parser';
import { parseM3U, groupChannelsByName, type M3UChannel } from '@/lib/utils/m3u-parser';
export interface IPTVSource {
id: string;
@@ -32,7 +32,6 @@ interface IPTVActions {
interface IPTVStore extends IPTVState, IPTVActions {}
const MAX_CONCURRENT = 3;
const MAX_CHANNELS = 5000; // Safety limit to prevent UI freeze
async function fetchWithConcurrencyLimit<T>(
tasks: (() => Promise<T>)[],
@@ -111,13 +110,11 @@ export const useIPTVStore = create<IPTVStore>()(
await fetchWithConcurrencyLimit(tasks, MAX_CONCURRENT);
// Limit total channels for performance
const finalChannels = allChannels.length > MAX_CHANNELS
? allChannels.slice(0, MAX_CHANNELS)
: allChannels;
// Group channels with the same name into multi-route entries
const grouped = groupChannelsByName(allChannels);
set({
cachedChannels: finalChannels,
cachedChannels: grouped,
cachedGroups: Array.from(allGroups).sort(),
lastRefreshed: Date.now(),
isLoading: false,
+35
View File
@@ -10,6 +10,7 @@ export interface M3UChannel {
group?: string;
tvgId?: string;
tvgName?: string;
routes?: string[];
}
export interface M3UPlaylist {
@@ -76,3 +77,37 @@ export function parseM3U(content: string): M3UPlaylist {
groups: Array.from(groupSet).sort(),
};
}
/**
* Group channels with the same name into single entries with multiple routes.
* This merges duplicate channel names (common in M3U playlists with multiple streams).
*/
export function groupChannelsByName(channels: M3UChannel[]): M3UChannel[] {
const groups = new Map<string, M3UChannel>();
for (const ch of channels) {
const key = ch.name.toLowerCase().trim();
const existing = groups.get(key);
if (existing) {
if (!existing.routes) {
existing.routes = [existing.url];
}
if (!existing.routes.includes(ch.url)) {
existing.routes.push(ch.url);
}
// Use first logo found
if (!existing.logo && ch.logo) existing.logo = ch.logo;
} else {
groups.set(key, { ...ch });
}
}
// Only add routes array when there are multiple routes
const result = Array.from(groups.values());
for (const ch of result) {
if (ch.routes && ch.routes.length <= 1) {
delete ch.routes;
}
}
return result;
}