html版
litv.m3u替换成你的m3u文件名
fetch('live.m3u')完整代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网页播放器</title>
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
font-family: Arial, sans-serif;
overflow: hidden;
background-color: #000; /* 确保背景为黑色 */
}
#app {
display: flex;
height: 100%;
position: relative;
background-color: #000; /* 确保app容器也是黑色 */
}
#sidebar {
position: fixed;
left: -250px;
width: 250px;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
color: white;
overflow-y: auto;
transition: left 0.3s ease;
z-index: 10;
}
#sidebar-button {
position: fixed;
left: 10px;
top: 10px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
padding: 10px;
cursor: pointer;
z-index: 15;
transition: opacity 0.3s ease;
}
#sidebar.active {
left: 0;
}
#channel-list {
padding: 20px;
padding-top: 60px; /* 为按钮留出空间 */
}
.channel-item {
display: flex;
align-items: center;
margin-bottom: 10px;
cursor: pointer;
}
.channel-item img {
width: 50px; /* 固定宽度 */
height: 50px; /* 固定高度 */
object-fit: contain; /* 保持图像比例 */
margin-right: 10px;
}
.channel-item span {
flex-grow: 1;
color: white;
}
#player-container {
flex-grow: 1;
position: relative;
width: 100%;
height: 100%;
background-color: #000; /* 确保播放器容器也是黑色 */
}
#video-player {
width: 100%;
height: 100%;
object-fit: cover;
background-color: #000; /* 确保视频播放器背景也是黑色 */
}
/* 针对小屏设备的自适应 */
@media (max-width: 768px) {
#sidebar {
width: 200px;
}
.channel-item img {
width: 40px;
height: 40px;
}
#sidebar-button {
padding: 8px;
}
#channel-list {
padding-top: 50px; /* 为按钮留出更小的空间 */
}
}
</style>
</head>
<body>
<div id="app">
<button id="sidebar-button">☰ 频道列表</button>
<div id="sidebar">
<div id="channel-list"></div>
</div>
<div id="player-container">
<video id="video-player" controls></video>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const channelList = document.getElementById('channel-list');
const videoPlayer = document.getElementById('video-player');
const sidebar = document.getElementById('sidebar');
const sidebarButton = document.getElementById('sidebar-button');
const app = document.getElementById('app');
// 点击按钮显示/隐藏侧边栏
sidebarButton.addEventListener('click', () => {
sidebar.classList.toggle('active');
});
// 鼠标移动时显示按钮,离开时隐藏按钮
let timeout;
app.addEventListener('mousemove', () => {
sidebarButton.style.opacity = '1';
clearTimeout(timeout);
timeout = setTimeout(() => {
sidebarButton.style.opacity = '0';
}, 3000);
});
app.addEventListener('mouseleave', () => {
sidebarButton.style.opacity = '0';
});
// 全屏变化时隐藏按钮
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
sidebarButton.style.opacity = '0';
}
});
console.log('开始加载 M3U 文件');
fetch('live.m3u')
.then(response => {
console.log('M3U 文件响应状态:', response.status);
return response.text();
})
.then(data => {
console.log('M3U 文件加载成功,内容长度:', data.length);
const channels = parseM3U(data);
console.log('解析后的频道数量:', channels.length);
renderChannelList(channels);
})
.catch(error => console.error('加载 M3U 文件时出错:', error));
function parseM3U(content) {
const lines = content.split('\n');
const channels = [];
let currentChannel = {};
for (const line of lines) {
if (line.startsWith('#EXTINF:')) {
const match = line.match(/tvg-id="([^"]*)" tvg-name="([^"]*)" tvg-logo="([^"]*)" group-title="([^"]*)", (.*)/);
if (match) {
currentChannel = {
id: match[1],
name: match[2],
logo: match[3],
group: match[4],
displayName: match[5]
};
} else {
console.warn('无法解析的 EXTINF 行:', line);
}
} else if (line.trim() !== '' && !line.startsWith('#')) {
currentChannel.url = line.trim();
channels.push(currentChannel);
currentChannel = {};
}
}
return channels;
}
function renderChannelList(channels) {
console.log('开始渲染频道列表');
channelList.innerHTML = ''; // 清空现有内容
channels.forEach(channel => {
const channelItem = document.createElement('div');
channelItem.className = 'channel-item';
channelItem.innerHTML =
`<img src="${channel.logo}" alt="${channel.name}" onerror="this.src='default-icon.png'">
<span>${channel.displayName}</span>`;
channelItem.addEventListener('click', () => {
playChannel(channel.url);
sidebar.classList.remove('active'); // 点击频道后关闭侧边栏
});
channelList.appendChild(channelItem);
});
console.log('频道列表渲染完成,共渲染', channels.length, '个频道');
}
function playChannel(url) {
console.log('尝试播放频道:', url);
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(url);
hls.attachMedia(videoPlayer);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
console.log('HLS 播放器加载成功');
videoPlayer.play().catch(error => console.error('播放失败:', error));
});
hls.on(Hls.Events.ERROR, function(event, data) {
console.error('HLS 播放器错误:', data.fatal, data);
});
} else if (videoPlayer.canPlayType('application/vnd.apple.mpegurl')) {
videoPlayer.src = url;
videoPlayer.addEventListener('loadedmetadata', function() {
console.log('视频元数据加载完成');
videoPlayer.play().catch(error => console.error('播放失败:', error));
});
videoPlayer.addEventListener('error', function(event) {
console.error('视频播放错误:', event);
});
} else {
console.error('此浏览器不支持 HLS');
}
}
});
</script>
</body>
</html>php版
m3u和php在同一目录
proxy_channel.php
<?php
// proxy_channel.php
// 从请求中获取目标 URL
$targetUrl = $_GET['url'] ?? '';
// 验证 URL 是否为空
if (empty($targetUrl)) {
http_response_code(400);
echo 'Missing URL parameter.';
exit;
}
// 检查目标 URL 是否有效
if (!filter_var($targetUrl, FILTER_VALIDATE_URL)) {
http_response_code(400);
echo 'Invalid URL.';
exit;
}
// 设置适当的超时
$timeout = 30;
// 初始化 cURL
$ch = curl_init($targetUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// 执行 cURL 请求
$response = curl_exec($ch);
// 检查是否发生错误
if (curl_errno($ch)) {
http_response_code(500);
echo 'Error fetching data: ' . curl_error($ch);
curl_close($ch);
exit;
}
// 获取响应头部内容
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
// 关闭 cURL
curl_close($ch);
// 设置响应头部
header('Content-Type: ' . $contentType);
// 输出响应内容
echo $response;
?>proxy_m3u_with_token.php
修改两处
1.litv.m3u替换成你的m3u文件名
$file = 'litv.m3u';2.hello替换成你设定的token
$validToken = 'hello'; // 替换为你实际使用的令牌完整的代码
<?php
// proxy_m3u_with_token.php
// M3U 文件的实际路径
$file = 'litv.m3u';
// 设置一个有效的令牌(应在服务器端安全地生成和存储)
$validToken = 'hello'; // 替换为你实际使用的令牌
// 检查请求中是否包含 token 参数,并验证其有效性
if (isset($_GET['token']) && $_GET['token'] === $validToken) {
// 检查 M3U 文件是否存在
if (file_exists($file)) {
// 设置响应头部为 M3U 文件的 MIME 类型
header('Content-Type: application/vnd.apple.mpegurl');
// 输出 M3U 文件内容
readfile($file);
} else {
// 文件未找到,返回 404 状态码
http_response_code(404);
echo 'File not found.';
}
} else {
// 令牌无效,返回 403 状态码
http_response_code(403);
echo 'Access denied.';
}
?>index.php
修改1处,将hello修改为proxy_m3u_with_token.php里设定的tkone
const token = '<?php echo 'hello'; ?>'; // 替换为你实际使用的令牌完整代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TVPLUS</title>
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
font-family: Arial, sans-serif;
overflow: hidden;
background-color: #000; /* 确保背景为黑色 */
}
#app {
display: flex;
height: 100%;
position: relative;
background-color: #000; /* 确保app容器也是黑色 */
}
#sidebar {
position: absolute;
left: -250px;
width: 250px;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
color: white;
overflow-y: auto;
transition: left 0.3s ease;
z-index: 10;
}
#sidebar-button {
position: absolute;
left: 10px;
top: 10px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
padding: 10px;
cursor: pointer;
z-index: 15;
opacity: 0;
transition: opacity 0.3s ease;
}
#sidebar.active {
left: 0;
}
#channel-list {
padding: 20px;
padding-top: 60px; /* 为按钮留出空间 */
}
.channel-item {
display: flex;
align-items: center;
margin-bottom: 10px;
cursor: pointer;
}
.channel-item img {
width: 50px;
height: 50px;
object-fit: contain; /* 确保图片不变形 */
margin-right: 10px;
}
#player-container {
flex-grow: 1;
position: relative;
width: 100%;
height: 100%;
background-color: #000; /* 确保播放器容器也是黑色 */
}
#video-player {
width: 100%;
height: 100%;
object-fit: cover;
background-color: #000; /* 确保视频播放器背景也是黑色 */
}
</style>
</head>
<body>
<div id="app">
<button id="sidebar-button">☰ 频道列表</button>
<div id="sidebar">
<div id="channel-list"></div>
</div>
<div id="player-container">
<video id="video-player" controls></video>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const channelList = document.getElementById('channel-list');
const videoPlayer = document.getElementById('video-player');
const sidebar = document.getElementById('sidebar');
const sidebarButton = document.getElementById('sidebar-button');
const app = document.getElementById('app');
// 从 PHP 获取令牌
const token = '<?php echo 'hello'; ?>'; // 替换为你实际使用的令牌
// 点击按钮显示/隐藏侧边栏
sidebarButton.addEventListener('click', () => {
sidebar.classList.toggle('active');
});
// 鼠标移动时显示按钮,离开时隐藏按钮
let timeout;
app.addEventListener('mousemove', () => {
sidebarButton.style.opacity = '1';
clearTimeout(timeout);
timeout = setTimeout(() => {
sidebarButton.style.opacity = '0';
}, 3000);
});
app.addEventListener('mouseleave', () => {
sidebarButton.style.opacity = '0';
});
// 全屏变化时隐藏按钮
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
sidebarButton.style.opacity = '0';
}
});
console.log('开始加载 M3U 文件');
fetch(`proxy_m3u_with_token.php?token=${token}`)
.then(response => {
console.log('M3U 文件响应状态:', response.status);
return response.text();
})
.then(data => {
console.log('M3U 文件加载成功,内容长度:', data.length);
const channels = parseM3U(data);
console.log('解析后的频道数量:', channels.length);
renderChannelList(channels);
})
.catch(error => console.error('加载 M3U 文件时出错:', error));
function parseM3U(content) {
const lines = content.split('\n');
const channels = [];
let currentChannel = {};
for (const line of lines) {
if (line.startsWith('#EXTINF:')) {
const match = line.match(/tvg-id="([^"]*)" tvg-name="([^"]*)" tvg-logo="([^"]*)" group-title="([^"]*)", (.*)/);
if (match) {
currentChannel = {
id: match[1],
name: match[2],
logo: match[3],
group: match[4],
displayName: match[5]
};
} else {
console.warn('无法解析的 EXTINF 行:', line);
}
} else if (line.trim() !== '' && !line.startsWith('#')) {
currentChannel.url = `proxy_channel.php?url=${encodeURIComponent(line.trim())}`;
channels.push(currentChannel);
currentChannel = {};
}
}
return channels;
}
function renderChannelList(channels) {
console.log('开始渲染频道列表');
channelList.innerHTML = ''; // 清空现有内容
channels.forEach(channel => {
const channelItem = document.createElement('div');
channelItem.className = 'channel-item';
channelItem.innerHTML = `
<img src="${channel.logo}" alt="${channel.name}" onerror="this.src='default-icon.png'">
<span>${channel.displayName}</span>
`;
channelItem.addEventListener('click', () => {
playChannel(channel.url);
sidebar.classList.remove('active'); // 点击频道后关闭侧边栏
});
channelList.appendChild(channelItem);
});
console.log('频道列表渲染完成,共渲染', channels.length, '个频道');
}
function playChannel(url) {
console.log('尝试播放频道:', url);
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(url);
hls.attachMedia(videoPlayer);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
videoPlayer.play();
});
} else if (videoPlayer.canPlayType('application/vnd.apple.mpegurl')) {
videoPlayer.src = url;
videoPlayer.addEventListener('loadedmetadata', function() {
videoPlayer.play();
});
} else {
console.error('此浏览器不支持 HLS');
}
}
});
</script>
</body>
</html>