吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 9585|回复: 161
收起左侧

[其他原创] 本地网页视频播放器 纯html

    [复制链接]
小智xyz 发表于 2025-6-13 20:44
本帖最后由 小智xyz 于 2025-8-2 09:47 编辑

吾爱这个代码好像会少东西,我这里复制下来使用发现,在线无法使用,可以去开源那里下载,或者,下载附件。
下载:
先放个开源链接,ps:(这里面有完全纯本地版本):https://github.com/xyz66882/Web-page-player
国内平台的开源链接:https://gitee.com/xyz66882/Web-page-player

index.zip (13.05 KB, 下载次数: 64)

index2.0.zip (15.68 KB, 下载次数: 21)

index3.0.zip (19.77 KB, 下载次数: 10)

index4.0.zip (20.26 KB, 下载次数: 11)

index5.0.zip (20.41 KB, 下载次数: 14)

index6.0.zip (25.76 KB, 下载次数: 14)

index7.0.zip (29.4 KB, 下载次数: 11)

index v7.1.zip (29.53 KB, 下载次数: 5)

index7.2.zip (31.82 KB, 下载次数: 5)

index v7.3.zip (31.88 KB, 下载次数: 7)

index v7.4.zip (32.6 KB, 下载次数: 14)

index v7.5.zip (33.31 KB, 下载次数: 6)

index v7.6.zip (34.11 KB, 下载次数: 3)

index v7.7.zip (34.32 KB, 下载次数: 32)

index v7.8.zip (35.96 KB, 下载次数: 6)

index v7.9.zip (36.28 KB, 下载次数: 225)


最初版本效果图:

测试.png
1.0:初次构建思路
2.0:更新历史播放名字显示完全,然后加一个可以添加备注功能。
3.0:更新加下载功能,支持分段下载视频例如(m3u8这种在线视频),和滤镜调节,画面调节功能。
4.0:修复手机浏览器不弹出下载,全屏滤镜调节,画面调节功能失效问题,不要用原生全屏键。
5.0:修复全屏显示画面不全,自适应显示完全
6.0:更新全新ui,支持手机ui适配,然后支持同一个网络链接视频,备注多个名字,然后新加锐化效果,字幕新加细线效果,反正看不清,新加播放,暂停按钮,新加重新加载按钮,修复视频播放可能加载失败问题。感谢,提供的排版参考和锐化滤镜设置

7.0:更新全新更好看ui,支持自定义背景链接,添加循环、列表、随机、播放功能,修复会有概率不显示视频内容。

7.1:修复播放历史按钮显示不完全,修复部分链接无法打开问题。

7.2:添加xgplayer.js支持了抖音等主流直播平台的支持

7.3:修复一下双击进入全屏和双击退出全屏失效问题

7.4:优化播放器状态提示,拖动进度条或使用左右方向键快进/快退时,显示“正在缓冲”以更准确反映实际加载状态。
    新增方向键快进/快退功能,操作逻辑与鼠标拖动一致,支持目标时间预览与长按连续跳帧。
    缓冲完成后自动恢复原播放或暂停状态,保持原有功能不变,提升用户体验与操作反馈一致性。

7.5:修复HLS错误: bufferStalledError,增加自动尝试恢复,从而减少因网络波动或短暂卡顿导致的播放中断。

7.6:修复下载失效问题,支持并行下载提升下载速度。

7.7:修复移动端不弹出下载问题。

7.8:修改视频进度条时间实时更新,添加显示系统时间按钮,有三种模式:带背景模式,无背景模式,隐藏模式,默认启用显示时间,更新测试链接为https开头。

7.9:修改一下移动端,全屏默认横屏,支持mkv的在线视频
添加了 以解决 CDN 视频因缺少 Referer 头无法播放的问题;移除了冗余的 flv.js 库及相关的 JavaScript 代码,FLV 播放现由 xgplayer 全权处理,代码更简洁、稳定。



下面是更新ui:

电脑.png 手机.png






[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>全能视频播放器</title>
  <!-- 引入外部资源 -->
  <link href="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-0-y/font-awesome/6.0.0/css/all.min.css" rel="stylesheet" />
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
  <script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-0-y/hls.js/8.0.0-beta.3/hls.js"></script>
  <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-0-y/flv.js/1.6.2/flv.js"></script>

  <style>
    /* 全局样式 */
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    :root {
      --primary: #4a6cf7;
      --primary-dark: #3a57d6;
      --text-primary: #1a202c;
      --text-secondary: #4a5568;
      --bg-light: #f8fafc;
      --bg-dark: #0f172a;
      --card-light: #ffffff;
      --card-dark: #1e293b;
      --border-light: #e2e8f0;
      --border-dark: #334155;
      --success: #10b981;
      --warning: #f59e0b;
      --danger: #ef4444;
      --shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
      --transition: all 0.3s ease;
    }

    body {
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
        sans-serif;
      background: var(--bg-light);
      color: var(--text-primary);
      line-height: 1.6;
      min-height: 100vh;
      padding: 20px;
      transition: var(--transition);
    }

    body.dark-mode {
      background: var(--bg-dark);
      color: #e2e8f0;
      --text-primary: #e2e8f0;
    }

    /* 布局 */
    .container {
      display: flex;
      flex-direction: column;
      max-width: 1200px;
      margin: 0 auto;
      gap: 20px;
    }

    .main-content {
      display: grid;
      grid-template-columns: 1fr;
      gap: 20px;
    }

    @media (min-width: 992px) {
      .main-content {
        grid-template-columns: 3fr 1fr;
      }
    }

    /* 卡片样式 */
    .card {
      background: var(--card-light);
      border-radius: 12px;
      box-shadow: var(--shadow);
      overflow: hidden;
      transition: var(--transition);
    }

    .dark-mode .card {
      background: var(--card-dark);
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
    }

    /* 头部 */
    header {
      padding: 20px 24px;
      background: var(--primary);
      color: white;
      display: flex;
      justify-content: space-between;
      align-items: center;
      position: relative;
    }

    .logo {
      display: flex;
      align-items: center;
      gap: 10px;
    }

    .logo i {
      font-size: 28px;
    }

    .logo h1 {
      font-size: 24px;
      font-weight: 700;
    }

    .theme-toggle {
      background: rgba(255, 255, 255, 0.2);
      border: none;
      width: 40px;
      height: 40px;
      border-radius: 50%;
      color: white;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: var(--transition);
    }

    .theme-toggle:hover {
      background: rgba(255, 255, 255, 0.3);
    }

    /* 播放器区域 */
    .video-container {
      position: relative;
      padding-top: 56.25%;
      background: #000;
      border-radius: 0 0 12px 12px;
      overflow: hidden;
    }

    #videoPlayer {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      outline: none;
    }

    /* 控制面板 */
    .control-panel {
      padding: 20px;
      display: flex;
      flex-direction: column;
      gap: 16px;
    }

    .input-group {
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
    }

    .input-group input {
      flex: 1;
      min-width: 200px;
      padding: 12px 16px;
      border: 1px solid var(--border-light);
      border-radius: 8px;
      font-size: 16px;
      background: transparent;
      color: var(--text-primary);
      transition: var(--transition);
    }

    .dark-mode .input-group input {
      border-color: var(--border-dark);
    }

    .input-group input:focus {
      border-color: var(--primary);
      outline: none;
    }

    .btn-group {
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
    }

    .btn {
      padding: 12px 20px;
      border: none;
      border-radius: 8px;
      font-size: 16px;
      font-weight: 500;
      cursor: pointer;
      display: flex;
      align-items: center;
      gap: 8px;
      transition: var(--transition);
    }

    .btn-primary {
      background: var(--primary);
      color: white;
    }

    .btn-primary:hover {
      background: var(--primary-dark);
    }

    .btn-secondary {
      background: #e2e8f0;
      color: var(--text-primary);
    }

    .dark-mode .btn-secondary {
      background: #334155;
    }

    .btn-secondary:hover {
      background: #cbd5e0;
    }

    .dark-mode .btn-secondary:hover {
      background: #475569;
    }

    .btn-danger {
      background: var(--danger);
      color: white;
    }

    .btn-danger:hover {
      background: #dc2626;
    }

    .btn-success {
      background: var(--success);
      color: white;
    }

    .btn-success:hover {
      background: #059669;
    }

    /* 功能区域 */
    .features {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 16px;
      margin-top: 10px;
    }

    .feature-card {
      background: rgba(74, 108, 247, 0.08);
      border-radius: 8px;
      padding: 16px;
      display: flex;
      flex-direction: column;
      align-items: center;
      text-align: center;
      transition: var(--transition);
    }

    .dark-mode .feature-card {
      background: rgba(74, 108, 247, 0.15);
    }

    .feature-card i {
      font-size: 32px;
      color: var(--primary);
      margin-bottom: 12px;
    }

    .feature-card h3 {
      font-size: 16px;
      margin-bottom: 8px;
    }

    .feature-card p {
      font-size: 14px;
      color: var(--text-secondary);
    }

    .dark-mode .feature-card p {
      color: #94a3b8;
    }

    /* 历史记录 */
    .history-section {
      background: var(--card-light);
      border-radius: 12px;
      box-shadow: var(--shadow);
      overflow: hidden;
    }

    .dark-mode .history-section {
      background: var(--card-dark);
    }

    .section-header {
      padding: 16px 20px;
      background: rgba(74, 108, 247, 0.1);
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .dark-mode .section-header {
      background: rgba(74, 108, 247, 0.15);
    }

    .section-header h2 {
      font-size: 18px;
      font-weight: 600;
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .history-list {
      padding: 16px;
      max-height: 400px;
      overflow-y: auto;
    }

    .history-item {
      padding: 12px;
      border-bottom: 1px solid var(--border-light);
      display: flex;
      justify-content: space-between;
      align-items: center;
      cursor: pointer;
      transition: var(--transition);
    }

    .dark-mode .history-item {
      border-bottom: 1px solid var(--border-dark);
    }

    .history-item:hover {
      background: rgba(74, 108, 247, 0.05);
    }

    .dark-mode .history-item:hover {
      background: rgba(74, 108, 247, 0.1);
    }

    .history-item .title {
      flex: 1;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      margin-right: 10px;
    }

    .history-item .actions {
      display: flex;
      gap: 8px;
    }

    .history-item .actions button {
      background: none;
      border: none;
      color: var(--text-secondary);
      cursor: pointer;
      width: 24px;
      height: 24px;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 4px;
      transition: var(--transition);
    }

    .dark-mode .history-item .actions button {
      color: #94a3b8;
    }

    .history-item .actions button:hover {
      background: rgba(0, 0, 0, 0.1);
      color: var(--danger);
    }

    .dark-mode .history-item .actions button:hover {
      background: rgba(255, 255, 255, 0.1);
    }

    .empty-history {
      padding: 40px 20px;
      text-align: center;
      color: var(--text-secondary);
    }

    .dark-mode .empty-history {
      color: #94a3b8;
    }

    /* 上传区域 */
    .upload-area {
      border: 2px dashed var(--border-light);
      border-radius: 8px;
      padding: 30px;
      text-align: center;
      cursor: pointer;
      transition: var(--transition);
      margin-top: 20px;
      position: relative;
    }

    .dark-mode .upload-area {
      border-color: var(--border-dark);
    }

    .upload-area:hover,
    .upload-area.drag-over {
      border-color: var(--primary);
      background: rgba(74, 108, 247, 0.05);
    }

    .dark-mode .upload-area:hover,
    .dark-mode .upload-area.drag-over {
      background: rgba(74, 108, 247, 0.1);
    }

    .upload-area i {
      font-size: 48px;
      color: var(--primary);
      margin-bottom: 16px;
    }

    .upload-area h3 {
      font-size: 18px;
      margin-bottom: 8px;
    }

    .upload-area p {
      color: var(--text-secondary);
      margin-bottom: 16px;
    }

    .dark-mode .upload-area p {
      color: #94a3b8;
    }

    /* 响应式调整 */
    @media (max-width: 768px) {
      .container {
        padding: 10px;
      }

      .btn {
        padding: 10px 16px;
        font-size: 14px;
      }

      .input-group input {
        font-size: 14px;
      }

      .features {
        grid-template-columns: 1fr;
      }

      .logo h1 {
        font-size: 20px;
      }

      .upload-area {
        padding: 20px;
      }

      .volume-controls {
        flex-direction: column;
        align-items: flex-start;
      }

      .subtitle-controls,
      .subtitle-settings {
        flex-direction: column;
      }
    }

    /* 状态指示器 */
    .status-indicator {
      padding: 8px 16px;
      border-radius: 20px;
      font-size: 14px;
      font-weight: 500;
      display: inline-flex;
      align-items: center;
      gap: 6px;
      margin-top: 10px;
    }

    .status-indicator.playing {
      background: rgba(16, 185, 129, 0.15);
      color: var(--success);
    }

    .status-indicator.stopped {
      background: rgba(239, 68, 68, 0.15);
      color: var(--danger);
    }

    .status-indicator.loading {
      background: rgba(245, 158, 11, 0.15);
      color: var(--warning);
    }

    .status-indicator i {
      animation: pulse 1.5s infinite;
    }

    @keyframes pulse {
      0% {
        opacity: 1;
      }

      50% {
        opacity: 0.5;
      }

      100% {
        opacity: 1;
      }
    }

    /* 进度条 (新样式) */
    .progress-controls {
      display: flex;
      align-items: center;
      gap: 10px;
      margin-top: 10px;
    }

    .seek-slider {
      flex: 1;
      height: 6px;
      -webkit-appearance: none;
      background: #e2e8f0;
      border-radius: 3px;
      outline: none;
    }

    .dark-mode .seek-slider {
      background: #334155;
    }

    .seek-slider::-webkit-slider-thumb {
      -webkit-appearance: none;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      background: var(--primary);
      cursor: pointer;
      transition: var(--transition);
    }

    .seek-slider::-webkit-slider-thumb:hover {
      transform: scale(1.2);
    }

    .time-display {
      font-size: 14px;
      font-weight: 500;
      min-width: 45px; /* Adjust as needed for 00:00 format */
      text-align: center;
    }


    /* 音量控制 */
    .volume-controls {
      display: flex;
      align-items: center;
      gap: 10px;
      margin-top: 10px;
    }

    .volume-slider-container {
      flex: 1;
      display: flex;
      align-items: center;
      gap: 10px;
    }

    .volume-slider {
      flex: 1;
      height: 6px;
      -webkit-appearance: none;
      background: #e2e8f0;
      border-radius: 3px;
      outline: none;
    }

    .dark-mode .volume-slider {
      background: #334155;
    }

    .volume-slider::-webkit-slider-thumb {
      -webkit-appearance: none;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      background: var(--primary);
      cursor: pointer;
      transition: var(--transition);
    }

    .volume-slider::-webkit-slider-thumb:hover {
      transform: scale(1.2);
    }

    .volume-percentage {
      min-width: 40px;
      text-align: right;
      font-size: 14px;
      font-weight: 500;
    }

    /* 字幕控制 */
    .subtitle-controls,
    .subtitle-settings {
      display: flex;
      align-items: center;
      /* Align items vertically */
      gap: 10px;
      margin-top: 10px;
      flex-wrap: wrap;
      /* Allow wrapping on smaller screens */
    }

    .subtitle-select {
      flex: 1;
      min-width: 150px;
      /* Ensure it doesn't get too small */
      padding: 8px 12px;
      border: 1px solid var(--border-light);
      border-radius: 8px;
      background: transparent;
      color: var(--text-primary);
      font-size: 14px;
      -webkit-appearance: none;
      /* Remove default select arrow */
      -moz-appearance: none;
      appearance: none;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%234a5568'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
      /* Custom arrow */
      background-repeat: no-repeat;
      background-position: right 12px center;
      background-size: 16px;
      padding-right: 30px;
      /* Make space for the arrow */
    }

    .dark-mode .subtitle-select {
      border-color: var(--border-dark);
      background-color: var(--card-dark);
      /* Fix white background in dark mode */
      color: var(--text-primary);
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2394a3b8'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
      /* Dark mode arrow */
    }

    .subtitle-settings label {
      font-size: 14px;
      color: var(--text-secondary);
      white-space: nowrap;
    }

    .dark-mode .subtitle-settings label {
      color: #94a3b8;
    }

    .subtitle-settings input[type='range'] {
      flex: 1;
      min-width: 80px;
      height: 6px;
      -webkit-appearance: none;
      background: #e2e8f0;
      border-radius: 3px;
      outline: none;
    }

    .dark-mode .subtitle-settings input[type='range'] {
      background: #334155;
    }

    .subtitle-settings input[type='range']::-webkit-slider-thumb {
      -webkit-appearance: none;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      background: var(--primary);
      cursor: pointer;
      transition: var(--transition);
    }

    .subtitle-settings input[type='range']::-webkit-slider-thumb:hover {
      transform: scale(1.2);
    }

    .subtitle-settings input[type='number'] {
      width: 50px;
      padding: 6px 8px;
      border: 1px solid var(--border-light);
      border-radius: 8px;
      font-size: 14px;
      background: transparent;
      color: var(--text-primary);
      text-align: center;
    }

    .dark-mode .subtitle-settings input[type='number'] {
      border-color: var(--border-dark);
    }

    .subtitle-settings input[type='color'] {
      width: 40px;
      height: 40px;
      border: none;
      padding: 0;
      background: none;
      cursor: pointer;
      border-radius: 8px;
      /* Make it match other inputs */
      overflow: hidden;
      /* Hide color picker default border */
    }
    .subtitle-settings input[type='color']::-webkit-color-swatch-wrapper {
      padding: 0;
    }
    .subtitle-settings input[type='color']::-webkit-color-swatch {
      border: none;
      border-radius: 8px;
    }

    /* 文件选择按钮 */
    .file-input-wrapper {
      position: relative;
      overflow: hidden;
      display: inline-block;
    }

    .file-input-wrapper input[type=file] {
      position: absolute;
      left: 0;
      top: 0;
      opacity: 0;
      width: 100%;
      height: 100%;
      cursor: pointer;
    }

    /* 格式标签 */
    .format-tag {
      display: inline-block;
      padding: 4px 8px;
      border-radius: 4px;
      background: rgba(74, 108, 247, 0.1);
      color: var(--primary);
      font-size: 12px;
      font-weight: 500;
      margin-left: 8px;
    }

    .dark-mode .format-tag {
      background: rgba(74, 108, 247, 0.2);
    }

    /* 示例链接 */
    .example-links {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      margin-top: 10px;
    }

    .example-link {
      padding: 6px 12px;
      background: rgba(74, 108, 247, 0.1);
      border-radius: 20px;
      color: var(--primary);
      font-size: 12px;
      cursor: pointer;
      transition: var(--transition);
    }

    .dark-mode .example-link {
      background: rgba(74, 108, 247, 0.2);
    }

    .example-link:hover {
      background: rgba(74, 108, 247, 0.2);
    }

    .dark-mode .example-link:hover {
      background: rgba(74, 108, 247, 0.3);
    }

    /* 字幕样式 */
    .subtitle-display {
      position: absolute;
      bottom: 60px;
      /* Adjust as needed */
      left: 50%;
      transform: translateX(-50%);
      width: auto;
      /* Allow width to adjust to content */
      max-width: 90%;
      /* Limit width */
      padding: 8px 12px;
      /* background: rgba(0, 0, 0, 0.8); removed black background */
      color: white;
      /* Default color, overridden by JS */
      font-size: 20px;
      /* Default size, overridden by JS */
      font-weight: 500;
      z-index: 10;
      border-radius: 6px;
      /* Slightly less rounded corners */
      text-align: center;
      line-height: 1.4;
      pointer-events: none;
      /* Allow clicks to pass through to video */
      display: none;
      /* Hidden by default */
      text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9), -1px -1px 2px rgba(0, 0, 0, 0.9), 1px -1px 2px rgba(0, 0, 0, 0.9),
        -1px 1px 2px rgba(0, 0, 0, 0.9);
      /* Stronger text shadow for readability without background */
    }

    /* 格式支持提示 */
    .format-support {
      margin-top: 15px;
      padding: 10px;
      background: rgba(74, 108, 247, 0.05);
      border-radius: 8px;
      font-size: 14px;
      color: var(--text-secondary);
    }

    .dark-mode .format-support {
      background: rgba(74, 108, 247, 0.1);
      color: #94a3b8;
    }

    .supported-formats {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin-top: 8px;
    }

    .format-badge {
      padding: 4px 8px;
      background: rgba(74, 108, 247, 0.1);
      border-radius: 4px;
      color: var(--primary);
      font-size: 12px;
    }

    .dark-mode .format-badge {
      background: rgba(74, 108, 247, 0.2);
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="card">
      <header>
        <div class="logo">
          <i class="fas fa-play-circle"></i>
          <h1>全能视频播放器</h1>
        </div>
        <button class="theme-toggle" id="themeToggle">
          <i class="fas fa-moon"></i>
        </button>
      </header>

      <div class="video-container">
        <video id="videoPlayer" controls></video>
        <div id="subtitleDisplay" class="subtitle-display" style="display: none;"></div>
      </div>

      <div class="control-panel">
        <div class="input-group">
          <input
            type="text"
            id="videoURL"
            placeholder="输入视频URL (支持所有主流视频格式)"
            value="http://cloud.ruiboyun.net/vod/vod3/e0u83v08/index.m3u8"
          />
          <button class="btn btn-primary">
            <i class="fas fa-play"></i> 播放
          </button>
          <button class="btn btn-secondary">
            <i class="fas fa-stop"></i> 停止
          </button>
          <div class="file-input-wrapper">
            <button class="btn btn-success" id="uploadLocalBtn">
              <i class="fas fa-upload"></i> 上传本地视频
            </button>
            <input
              type="file"
              id="fileInput"
              accept="video/mp4,video/webm,video/ogg,video/x-flv,video/quicktime,video/x-msvideo,video/x-ms-wmv,video/3gpp,video/x-matroska,video/avi,video/mpeg,video/mov"
            />
          </div>
        </div>



        <div class="subtitle-controls">
          <select id="subtitleSelect" class="subtitle-select">
            <option value="none">无字幕</option>
            <!-- 上传字幕选项不在这里,改成按钮 -->
          </select>
          <button class="btn btn-secondary" id="uploadSubtitleBtn">
            <i class="fas fa-file-upload"></i> 上传字幕文件 (.srt, .vtt)
          </button>
          <button class="btn btn-secondary" id="toggleSubtitleBtn">
            <i class="fas fa-eye-slash"></i> 显示/隐藏字幕
          </button>
        </div>

        <div class="subtitle-settings">
          <label for="subtitleSizeSlider">字幕大小:</label>
          <input type="range" id="subtitleSizeSlider" min="12" max="48" value="20" />
          <input
            type="number"
            id="subtitleSizeValue"
            min="12"
            max="48"
            value="20"
            class="small-input"
            style="width: 50px;"
          />
          px

          <label for="subtitleColorPicker">颜色:</label>
          <input type="color" id="subtitleColorPicker" value="#FFFFFF" />
        </div>

        <div class="volume-controls">
          <div class="volume-slider-container">
            <i class="fas fa-volume-up" id="volumeIcon"></i>
            <input type="range" min="0" max="100" value="100" class="volume-slider" id="volumeSlider">
            <div class="volume-percentage" id="volumePercentage">100%</div>
          </div>
          <button class="btn btn-secondary">
            <i class="fas fa-volume-up" id="muteButtonIcon"></i> 静音
          </button>
        </div>

        <div class="btn-group">
          <button class="btn btn-secondary">
            <i class="fas fa-expand"></i> 全屏
          </button>
          <button class="btn btn-secondary">
            <i class="fas fa-tachometer-alt"></i> 0.5x
          </button>
          <button class="btn btn-secondary">
            <i class="fas fa-tachometer-alt"></i> 1x
          </button>
          <button class="btn btn-secondary">
            <i class="fas fa-tachometer-alt"></i> 1.5x
          </button>
          <button class="btn btn-secondary">
            <i class="fas fa-tachometer-alt"></i> 2x
          </button>
        </div>

        <div id="statusIndicator" class="status-indicator stopped">
          <i class="fas fa-circle"></i> 播放器已停止
        </div>

        <!-- 新的视频进度条和时间显示 -->
        <div class="progress-controls">
          <div id="currentTimeDisplay" class="time-display">00:00</div>
          <input type="range" min="0" max="0" value="0" step="0.01" class="seek-slider" id="seekSlider">
          <div id="durationDisplay" class="time-display">00:00</div>
        </div>

        <div class="features">
          <div class="feature-card">
            <i class="fas fa-film"></i>
            <h3>全格式支持</h3>
            <p>支持所有主流视频格式</p>
          </div>
          <div class="feature-card">
            <i class="fas fa-closed-captioning"></i>
            <h3>完整字幕功能</h3>
            <p>外挂字幕支持,可调样式</p>
          </div>
          <div class="feature-card">
            <i class="fas fa-sliders-h"></i>
            <h3>高级控制</h3>
            <p>无极音量调节,播放速度控制</p>
          </div>
          <div class="feature-card">
            <i class="fas fa-cloud"></i>
            <h3>在线播放</h3>
            <p>支持所有主流视频链接</p>
          </div>
        </div>

        <div class="format-support">
          <p><strong>支持的视频格式:</strong></p>
          <div class="supported-formats">
            <span class="format-badge">MP4</span>
            <span class="format-badge">M3U8</span>
            <span class="format-badge">FLV</span>
            <span class="format-badge">WebM</span>
            <span class="format-badge">MKV</span>
            <span class="format-badge">MOV</span>
            <span class="format-badge">AVI</span>
            <span class="format-badge">WMV</span>
            <span class="format-badge">3GP</span>
            <span class="format-badge">OGG</span>
            <span class="format-badge">MPEG</span>
            <span class="format-badge">VOB</span>
          </div>
        </div>
      </div>
    </div>

    <div class="main-content">
      <div class="card">
        <div class="upload-area" id="uploadArea">
          <i class="fas fa-cloud-upload-alt"></i>
          <h3>上传本地视频</h3>
          <p>点击或拖放视频文件到此处</p>
          <p class="text-small">支持所有主流视频格式 (如 MP4, MKV, FLV, WebM 等)</p>
        </div>
      </div>

      <div class="history-section">
        <div class="section-header">
          <h2><i class="fas fa-history"></i> 播放历史</h2>
          <button class="btn btn-danger">
            <i class="fas fa-trash"></i> 清空
          </button>
        </div>
        <div class="history-list" id="historyList">
          <!-- 历史记录将通过JS动态添加 -->
        </div>
      </div>
    </div>
  </div>

  <script>
    let hlsInstance = null;
    let flvPlayer = null;
    let currentVideo = null;
    let playbackHistory = JSON.parse(localStorage.getItem('playbackHistory')) || [];
    let currentSubtitleTracks = []; // 存储解析后的字幕对象 { start, end, text }
    let subtitleActive = false; // 控制自定义字幕显示是否激活
    let currentSubtitleFileName = null; // 当前加载的字幕文件名
    let isSeeking = false; // 用于标记用户是否正在拖动进度条

    // 支持的视频格式列表 (用于文件扩展名检查)
    const supportedFormats = [
      'mp4',
      'm3u8',
      'flv',
      'webm',
      'mkv',
      'mov',
      'avi',
      'wmv',
      '3gp',
      'ogg',
      'mpg',
      'mpeg',
      'vob',
    ];

    // 字幕设置的默认值
    const defaultSubtitleSettings = {
      fontSize: 20,
      color: '#FFFFFF',
    };
    let currentSubtitleSettings = JSON.parse(localStorage.getItem('subtitleSettings')) || defaultSubtitleSettings;

    // 初始化页面
    document.addEventListener('DOMContentLoaded', function () {
      // 加载主题设置
      loadTheme();

      // 渲染历史记录
      renderHistory();

      // 设置上传区域功能
      setupUploadArea();

      // 设置播放器事件监听 (包含进度条)
      setupPlayerEvents();

      // 设置音量控制
      setupVolumeControl();

      // 设置字幕样式控制
      setupSubtitleSettings();

      // 绑定上传本地视频按钮
      document.getElementById('uploadLocalBtn').addEventListener('click', () => {
        document.getElementById('fileInput').click();
      });

      // 绑定上传字幕按钮
      document.getElementById('uploadSubtitleBtn').addEventListener('click', () => {
        uploadSubtitleFile();
      });

      // 监听本地视频文件选择
      document.getElementById('fileInput').addEventListener('change', function () {
        if (this.files && this.files[0]) {
          playLocalVideo(this.files[0]);
          this.value = ''; // 清空,允许重复上传同一个文件也触发
        }
      });

      // 初始化字幕显示/隐藏按钮的图标
      updateToggleSubtitleButtonIcon();
      // 初始化静音按钮的图标
      updateMuteButtonIcon();
    });

    // 设置上传区域功能
    function setupUploadArea() {
      const uploadArea = document.getElementById('uploadArea');
      const fileInput = document.getElementById('fileInput');

      // 点击上传区域触发文件选择
      uploadArea.addEventListener('click', () => {
        fileInput.click();
      });

      // 拖放功能
      ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
        uploadArea.addEventListener(eventName, preventDefaults, false);
      });

      function preventDefaults(e) {
        e.preventDefault();
        e.stopPropagation();
      }

      ['dragenter', 'dragover'].forEach((eventName) => {
        uploadArea.addEventListener(
          eventName,
          () => {
            uploadArea.classList.add('drag-over');
          },
          false
        );
      });

      ['dragleave', 'drop'].forEach((eventName) => {
        uploadArea.addEventListener(
          eventName,
          () => {
            uploadArea.classList.remove('drag-over');
          },
          false
        );
      });

      uploadArea.addEventListener(
        'drop',
        function handleDrop(e) {
          const dt = e.dataTransfer;
          const files = dt.files;

          if (files && files.length) {
            playLocalVideo(files[0]);
          }
        },
        false
      );
    }

    // 设置播放器事件监听
    function setupPlayerEvents() {
      const video = document.getElementById('videoPlayer');
      const seekSlider = document.getElementById('seekSlider');
      const currentTimeDisplay = document.getElementById('currentTimeDisplay');
      const durationDisplay = document.getElementById('durationDisplay');

      video.addEventListener('timeupdate', function () {
        if (!isSeeking) { // 只有当用户没有拖动进度条时,才更新滑块位置
          seekSlider.value = video.currentTime;
        }
        currentTimeDisplay.textContent = formatTime(video.currentTime);

        // 更新自定义字幕显示
        if (subtitleActive && currentSubtitleTracks.length > 0) {
          updateSubtitleDisplay(this.currentTime);
        } else {
          // 如果没有激活自定义字幕或没有字幕数据,确保自定义字幕显示区域隐藏
          document.getElementById('subtitleDisplay').textContent = '';
          document.getElementById('subtitleDisplay').style.display = 'none';
        }
      });

      video.addEventListener('loadedmetadata', function() {
        seekSlider.max = video.duration;
        durationDisplay.textContent = formatTime(video.duration);
        currentTimeDisplay.textContent = formatTime(video.currentTime); // 确保初始显示00:00
      });

      video.addEventListener('playing', function () {
        updateStatus('playing', '正在播放');
      });

      video.addEventListener('pause', function () {
        updateStatus('stopped', '播放已暂停');
      });

      video.addEventListener('waiting', function () {
        updateStatus('loading', '正在缓冲...');
      });

      video.addEventListener('ended', function () {
        updateStatus('stopped', '播放已结束');
        seekSlider.value = 0; // 播放结束后将进度条归零
        currentTimeDisplay.textContent = formatTime(0);
      });

      video.addEventListener('error', function () {
        const error = this.error;
        let errorMessage = '未知错误';
        if (error) {
          switch (error.code) {
            case error.MEDIA_ERR_ABORTED:
              errorMessage = '用户终止了播放。';
              break;
            case error.MEDIA_ERR_NETWORK:
              errorMessage = '网络错误导致视频下载失败。';
              break;
            case error.MEDIA_ERR_DECODE:
              errorMessage = '视频解码错误。';
              break;
            case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
              errorMessage = '视频源格式不受支持或文件损坏。';
              break;
            default:
              errorMessage = `错误代码: ${error.code} - ${error.message}`;
              break;
          }
        }
        console.error('播放错误:', error);
        updateStatus('stopped', '播放错误: ' + errorMessage);
      });

      // 进度条拖动事件
      seekSlider.addEventListener('mousedown', () => {
        isSeeking = true;
        video.pause(); // 拖动时暂停视频,提供更流畅的拖动体验
      });

      seekSlider.addEventListener('mouseup', () => {
        isSeeking = false;
        video.currentTime = seekSlider.value; // 设置视频当前时间为滑块值
        video.play(); // 恢复播放
      });

      seekSlider.addEventListener('input', () => {
        // 用户拖动滑块时,实时更新当前时间显示
        currentTimeDisplay.textContent = formatTime(seekSlider.value);
      });
    }

    // 将秒数格式化为 HH:MM:SS 或 MM:SS 格式
    function formatTime(seconds) {
      const minutes = Math.floor(seconds / 60);
      const remainingSeconds = Math.floor(seconds % 60);
      const hours = Math.floor(minutes / 60);
      const remainingMinutes = Math.floor(minutes % 60);

      if (hours > 0) {
        return `${hours.toString().padStart(2, '0')}:${remainingMinutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
      } else {
        return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
      }
    }

    // 设置音量控制
    function setupVolumeControl() {
      const video = document.getElementById('videoPlayer');
      const volumeSlider = document.getElementById('volumeSlider');
      const volumePercentage = document.getElementById('volumePercentage');
      const volumeIcon = document.getElementById('volumeIcon');

      // 初始化音量
      video.volume = volumeSlider.value / 100;
      volumePercentage.textContent = volumeSlider.value + '%';
      updateMuteButtonIcon(); // 确保静音按钮图标与初始音量状态一致

      // 音量滑块事件
      volumeSlider.addEventListener('input', function () {
        const volume = this.value / 100;
        video.volume = volume;
        volumePercentage.textContent = this.value + '%';
        
        // 更新音量图标 (滑块旁的图标)
        if (volume === 0) {
          volumeIcon.className = 'fas fa-volume-mute';
        } else if (volume < 0.5) {
          volumeIcon.className = 'fas fa-volume-down';
        } else {
          volumeIcon.className = 'fas fa-volume-up';
        }
        updateMuteButtonIcon(); // 音量变化时更新静音按钮图标
      });
    }

    // 设置字幕样式控制
    function setupSubtitleSettings() {
      const subtitleDisplay = document.getElementById('subtitleDisplay');
      const sizeSlider = document.getElementById('subtitleSizeSlider');
      const sizeValue = document.getElementById('subtitleSizeValue');
      const colorPicker = document.getElementById('subtitleColorPicker');

      // 初始化设置
      sizeSlider.value = currentSubtitleSettings.fontSize;
      sizeValue.value = currentSubtitleSettings.fontSize;
      colorPicker.value = currentSubtitleSettings.color;
      applySubtitleSettings();

      // 监听大小滑块
      sizeSlider.addEventListener('input', function () {
        sizeValue.value = this.value;
        currentSubtitleSettings.fontSize = parseInt(this.value);
        applySubtitleSettings();
        saveSubtitleSettings();
      });

      // 监听大小输入框
      sizeValue.addEventListener('change', function () {
        let val = parseInt(this.value);
        if (isNaN(val) || val < parseInt(sizeSlider.min) || val > parseInt(sizeSlider.max)) {
          val = defaultSubtitleSettings.fontSize;
          // 恢复默认或上次有效值
          this.value = val;
        }
        sizeSlider.value = val;
        currentSubtitleSettings.fontSize = val;
        applySubtitleSettings();
        saveSubtitleSettings();
      });

      // 监听颜色选择器
      colorPicker.addEventListener('input', function () {
        currentSubtitleSettings.color = this.value;
        applySubtitleSettings();
        saveSubtitleSettings();
      });
    }

    // 应用字幕样式
    function applySubtitleSettings() {
      const subtitleDisplay = document.getElementById('subtitleDisplay');
      subtitleDisplay.style.fontSize = `${currentSubtitleSettings.fontSize}px`;
      subtitleDisplay.style.color = currentSubtitleSettings.color;
    }

    // 保存字幕设置到 localStorage
    function saveSubtitleSettings() {
      localStorage.setItem('subtitleSettings', JSON.stringify(currentSubtitleSettings));
    }

    // 更新播放状态
    function updateStatus(status, message) {
      const indicator = document.getElementById('statusIndicator');
      indicator.className = `status-indicator ${status}`;
      indicator.innerHTML = `<i class="fas fa-circle"></i> ${message}`;
    }

    // 播放本地视频
    function playLocalVideo(file) {
      // 检查文件格式
      const extension = file.name.split('.').pop().toLowerCase();

      if (!supportedFormats.includes(extension)) {
        alert(`不支持的视频格式: .${extension}\n请尝试其他视频文件,或确保文件未损坏。`);
        updateStatus('stopped', `不支持的格式: .${extension}`);
        return;
      }

      const fileURL = URL.createObjectURL(file);

      // 停止之前的播放并清理
      stopPlaybackInternal();

      // 播放新视频
      play(fileURL);

      // 添加到历史记录
      addToHistory(`本地视频: ${file.name}`, fileURL);

      updateStatus('playing', `正在播放: ${file.name}`);
    }

    // 开始播放
    function startPlayback() {
      const url = document.getElementById('videoURL').value.trim();

      if (!url) {
        alert('请输入视频地址!');
        return;
      }

      // 添加到历史记录
      addToHistory(url, url);

      play(url);
    }

    // 加载示例视频
    function loadExample(url) {
      // 如果已经是当前播放,强制重载实现无缝切换
      // 注意:对于本地文件,URL.createObjectURL每次都会生成新的blob:URL,所以不能直接比较currentVideo === url
      // 但对于在线链接,这个比较是有效的。
      if (currentVideo === url && !url.startsWith('blob:')) {
        play(url); // 强制播放实现无缝切换
      } else {
        // 直接播放新链接
        document.getElementById('videoURL').value = url;
        play(url);
      }
    }

    /**
     * 自动检测并播放视频
     * @param {string} url - 视频地址
     */
    function play(url) {
      const video = document.getElementById('videoPlayer');

      // 如果正在加载或播放,先停止当前播放(支持无缝切换)
      stopPlaybackInternal();

      currentVideo = url;
      updateStatus('loading', '正在加载视频...'); // 立即显示加载状态,提供即时反馈

      // 自动检测格式
      if (url.startsWith('blob:')) {
        // 本地文件 (Blob URL)
        video.src = url;
        video.play().catch((e) => {
          console.error('本地视频播放错误:', e);
          updateStatus('stopped', '播放错误: ' + e.message);
        });
        return;
      }

      if (url.indexOf('rtmp://') === 0) {
        alert('当前浏览器不支持 RTMP 协议播放,请尝试其他视频格式!');
        updateStatus('stopped', '不支持RTMP协议');
        return;
      }

      // 获取文件扩展名
      const extension = getFileExtension(url);

      // 判断是否为 HLS(m3u8 格式)
      if (extension === 'm3u8' || url.includes('.m3u8?') || url.includes('m3u8')) {
        if (Hls.isSupported()) {
          hlsInstance = new Hls();
          hlsInstance.loadSource(url);
          hlsInstance.attachMedia(video);
          hlsInstance.on(Hls.Events.MANIFEST_PARSED, function () {
            video
              .play()
              .then(() => updateStatus('playing', '正在播放'))
              .catch((e) => {
                console.error('HLS播放错误:', e);
                updateStatus('stopped', '播放错误: ' + e.message);
              });
          });
          hlsInstance.on(Hls.Events.ERROR, function (event, data) {
            console.error('HLS错误:', data);
            updateStatus('stopped', 'HLS错误: ' + data.details);
          });
        } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
          // Safari 浏览器原生支持 HLS
          video.src = url;
          video
            .play()
            .then(() => updateStatus('playing', '正在播放'))
            .catch((e) => {
              console.error('原生HLS播放错误:', e);
              updateStatus('stopped', '播放错误: ' + e.message);
            });
        } else {
          alert('当前浏览器不支持 HLS 视频播放!');
          updateStatus('stopped', '不支持HLS播放');
          return;
        }
      }
      // 判断是否为 FLV 格式
      else if (extension === 'flv' || url.includes('.flv?')) {
        if (flvjs.isSupported()) {
          flvPlayer = flvjs.createPlayer({
            type: 'flv',
            url: url,
          });
          flvPlayer.attachMediaElement(video);
          flvPlayer.load();
          flvPlayer
            .play()
            .then(() => updateStatus('playing', '正在播放'))
            .catch((e) => {
              console.error('FLV播放错误:', e);
              updateStatus('stopped', '播放错误: ' + e.message);
            });
        } else {
          alert('当前浏览器不支持 FLV 视频播放!');
          updateStatus('stopped', '不支持FLV播放');
          return;
        }
      }
      // 其他格式(尝试浏览器原生支持)
      else {
        video.src = url;
        video
          .play()
          .then(() => updateStatus('playing', '正在播放'))
          .catch((e) => {
            console.error('原生播放错误:', e);
            updateStatus('stopped', '播放失败,尝试其他方式...');

            // 如果原生播放失败,尝试使用HLS.js (有时非标准MP4等也可以被HLS.js处理)
            if (Hls.isSupported()) {
              updateStatus('loading', '尝试使用HLS播放...');
              hlsInstance = new Hls();
              hlsInstance.loadSource(url);
              hlsInstance.attachMedia(video);
              hlsInstance.on(Hls.Events.MANIFEST_PARSED, function () {
                video
                  .play()
                  .then(() => updateStatus('playing', '正在播放'))
                  .catch((e) => {
                    console.error('HLS回退播放错误:', e);
                    updateStatus('stopped', '播放失败');
                  });
              });
              hlsInstance.on(Hls.Events.ERROR, function (event, data) {
                console.error('HLS回退错误:', data);
                updateStatus('stopped', 'HLS回退错误: ' + data.details);
              });
            } else {
              updateStatus('stopped', '播放失败,浏览器不支持此格式。');
            }
          });
      }
    }

    // 停止播放内部实现(不清空URL输入框)
    function stopPlaybackInternal() {
      const video = document.getElementById('videoPlayer');
      video.pause();
      video.currentTime = 0;
      document.getElementById('seekSlider').value = 0; // 停止时进度条归零
      document.getElementById('currentTimeDisplay').textContent = '00:00'; // 停止时时间显示归零
      document.getElementById('durationDisplay').textContent = '00:00'; // 停止时时长显示归零

      if (hlsInstance) {
        hlsInstance.destroy();
        hlsInstance = null;
      }
      if (flvPlayer) {
        flvPlayer.destroy();
        flvPlayer = null;
      }

      // 清理字幕
      removeSubtitle();

      updateStatus('stopped', '播放器已停止');
    }

    // 停止播放(外部接口)
    function stopPlayback() {
      stopPlaybackInternal();
    }

    // 获取文件扩展名
    function getFileExtension(url) {
      // 确保能正确处理带查询参数或哈希的URL
      const parts = url.split('.');
      if (parts.length > 1) {
        return parts.pop().split(/[#?]/)[0].toLowerCase();
      }
      return ''; // 没有扩展名
    }

    // Helper: 将时间字符串 (HH:MM:SS,ms 或 HH:MM:SS.ms) 转换为秒数
    function parseTime(timeStr) {
      const parts = timeStr.split(':');
      const secondsAndMs = parts[2].split(/[,.]/); // 同时处理逗号和点作为毫秒分隔符
      const hours = parseInt(parts[0], 10);
      const minutes = parseInt(parts[1], 10);
      const seconds = parseInt(secondsAndMs[0], 10);
      const milliseconds = parseInt(secondsAndMs[1], 10) || 0;
      return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000;
    }

    // 解析 SRT 或 VTT 内容为字幕对象数组
    function parseSrtOrVtt(content) {
      const subtitles = [];
      const lines = content.split(/\r?\n/); // 兼容不同换行符
      let i = 0;
      while (i < lines.length) {
        if (!lines[i].trim() || isNaN(parseInt(lines[i].trim(), 10))) {
          if (lines[i].trim().toUpperCase() === 'WEBVTT') {
            i++;
            continue;
          }
          if (lines[i].trim().startsWith('NOTE') || lines[i].trim().startsWith('STYLE')) {
            i++;
            continue;
          }
          if (!lines[i].trim().includes('-->') && lines[i].trim() !== '') {
            i++;
            continue;
          }
          i++;
          continue;
        }

        let timeLineFound = false;
        let timeLine = '';
        let textLines = [];
        while (i < lines.length && lines[i].trim() !== '') {
          if (lines[i].includes('-->')) {
            timeLine = lines[i].trim();
            timeLineFound = true;
            i++;
            break;
          }
          i++;
        }

        if (!timeLineFound) {
          continue;
        }

        while (i < lines.length && lines[i].trim() !== '') {
          textLines.push(lines[i].trim());
          i++;
        }

        const timeParts = timeLine.split(' --> ');
        if (timeParts.length === 2) {
          const start = parseTime(timeParts[0]);
          const end = parseTime(timeParts[1]);
          const text = textLines.join('\n');
          subtitles.push({ start, end, text });
        }
      }
      return subtitles;
    }

    // 上传字幕文件并加载
    function uploadSubtitleFile() {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = '.vtt,.srt';
      input.style.display = 'none';

      input.onchange = function (e) {
        const file = e.target.files[0];
        if (!file) return;

        const reader = new FileReader();
        reader.onload = function (event) {
          const content = event.target.result;
          parseAndActivateSubtitle(content);
          currentSubtitleFileName = file.name;
          updateSubtitleSelectLabel(file.name);
          alert('字幕文件已成功加载!');
        };
        reader.readAsText(file);
      };

      document.body.appendChild(input);
      input.click();
      document.body.removeChild(input);
    }

    // 更新字幕选择下拉显示文本(显示文件名,且禁用“上传字幕文件”选项)
    function updateSubtitleSelectLabel(fileName) {
      const subtitleSelect = document.getElementById('subtitleSelect');
      // 清空所有选项
      subtitleSelect.innerHTML = '';

      // 添加“无字幕”选项
      const noneOption = document.createElement('option');
      noneOption.value = 'none';
      noneOption.textContent = '无字幕';
      subtitleSelect.appendChild(noneOption);

      // 添加上传的字幕文件名作为唯一选项(选中)
      if (fileName) {
        const fileOption = document.createElement('option');
        fileOption.value = 'uploaded';
        fileOption.textContent = fileName;
        fileOption.selected = true;
        subtitleSelect.appendChild(fileOption);
      } else {
        noneOption.selected = true;
      }
    }

    // 解析并激活自定义字幕
    function parseAndActivateSubtitle(content) {
      currentSubtitleTracks = parseSrtOrVtt(content);
      subtitleActive = true;
      document.getElementById('subtitleDisplay').style.display = 'block';
      document.getElementById('subtitleDisplay').textContent = '';
      updateSubtitleDisplay(document.getElementById('videoPlayer').currentTime);
      applySubtitleSettings();
      updateToggleSubtitleButtonIcon(); // 字幕加载成功,更新图标为“显示”状态
    }

    // 根据当前播放时间更新自定义字幕显示
    function updateSubtitleDisplay(currentTime) {
      const display = document.getElementById('subtitleDisplay');
      let currentCaption = '';

      for (const track of currentSubtitleTracks) {
        if (currentTime >= track.start && currentTime <= track.end) {
          currentCaption = track.text;
          break;
        }
      }
      display.textContent = currentCaption;
      display.style.display = currentCaption ? 'block' : 'none';
    }

    // 切换字幕显示(仅针对自定义字幕)
    function toggleSubtitle() {
      const display = document.getElementById('subtitleDisplay');
      const video = document.getElementById('videoPlayer');

      if (currentSubtitleTracks.length === 0) {
        alert('没有加载外挂字幕文件。请先上传字幕文件。');
        subtitleActive = false; // 确保状态为未激活
        updateToggleSubtitleButtonIcon(); // 确保图标为“隐藏”状态
        display.style.display = 'none';
        return;
      }

      subtitleActive = !subtitleActive; // 切换状态
      
      if (subtitleActive) {
        updateSubtitleDisplay(video.currentTime);
        display.style.display = 'block';
      } else {
        display.style.display = 'none';
      }
      updateToggleSubtitleButtonIcon(); // 更新图标
    }

    // 更新显示/隐藏字幕按钮的图标
    function updateToggleSubtitleButtonIcon() {
      const toggleBtn = document.getElementById('toggleSubtitleBtn');
      if (toggleBtn) {
        const toggleIcon = toggleBtn.querySelector('i');
        if (toggleIcon) {
          if (subtitleActive) { // 字幕当前是激活(显示)状态
            toggleIcon.className = 'fas fa-eye'; // 图标显示正常眼睛,表示“当前可见”
          } else { // 字幕当前是未激活(隐藏)状态
            toggleIcon.className = 'fas fa-eye-slash'; // 图标显示带斜线眼睛,表示“当前隐藏”
          }
        }
      }
    }

    // 移除所有字幕(自定义)
    function removeSubtitle() {
      currentSubtitleTracks = [];
      subtitleActive = false;
      currentSubtitleFileName = null;
      document.getElementById('subtitleDisplay').textContent = '';
      document.getElementById('subtitleDisplay').style.display = 'none';

      // 确保原生文本轨道也被禁用,以防万一
      const video = document.getElementById('videoPlayer');
      for (let i = 0; i < video.textTracks.length; i++) {
        video.textTracks[i].mode = 'disabled';
      }

      // 重置字幕选择框为“无字幕”
      updateSubtitleSelectLabel(null);
      updateToggleSubtitleButtonIcon(); // 字幕已移除,更新图标为“隐藏”状态
    }

    // 添加到历史记录
    function addToHistory(title, url) {
      // 避免重复添加
      const existing = playbackHistory.find((item) => item.url === url);
      if (existing) {
        playbackHistory = playbackHistory.filter((item) => item.url !== url);
      }

      playbackHistory.unshift({
        title: title,
        url: url,
        timestamp: new Date().toISOString(),
      });

      if (playbackHistory.length > 10) {
        playbackHistory.pop();
      }

      localStorage.setItem('playbackHistory', JSON.stringify(playbackHistory));
      renderHistory();
    }

    // 渲染历史记录
    function renderHistory() {
      const historyList = document.getElementById('historyList');

      if (playbackHistory.length === 0) {
        historyList.innerHTML = '<div class="empty-history">暂无播放历史</div>';
        return;
      }

      historyList.innerHTML = '';

      playbackHistory.forEach((item, index) => {
        const historyItem = document.createElement('div');
        historyItem.className = 'history-item';

        let format = '未知';
        if (item.url.startsWith('blob:')) {
          format = '本地视频';
        } else {
          const ext = getFileExtension(item.url);
          if (ext) {
            format = ext.toUpperCase();
          }
        }

        historyItem.innerHTML = `
          <div class="title" title="${item.title}">${item.title}</div>
          <div class="format-tag">${format}</div>
          <div class="actions">
            <button title="播放"><i class="fas fa-play"></i></button>
            <button title="删除"><i class="fas fa-trash"></i></button>
          </div>
        `;
        historyList.appendChild(historyItem);
      });
    }

    // 从历史记录播放
    function playFromHistory(url) {
      if (currentVideo === url && !url.startsWith('blob:')) {
        play(url); // 强制播放实现无缝切换
      } else {
        document.getElementById('videoURL').value = url;
        play(url);
      }
    }

    // 删除历史记录项
    function removeHistoryItem(index) {
      playbackHistory.splice(index, 1);
      localStorage.setItem('playbackHistory', JSON.stringify(playbackHistory));
      renderHistory();
    }

    // 清空历史记录
    function clearHistory() {
      if (playbackHistory.length === 0) return;

      if (confirm('确定要清空所有播放历史吗?')) {
        playbackHistory = [];
        localStorage.removeItem('playbackHistory');
        renderHistory();
      }
    }

    // 切换全屏
    function toggleFullscreen() {
      const video = document.getElementById('videoPlayer');

      if (!document.fullscreenElement) {
        if (video.requestFullscreen) {
          video.requestFullscreen();
        } else if (video.mozRequestFullScreen) {
          video.mozRequestFullScreen();
        } else if (video.webkitRequestFullscreen) {
          video.webkitRequestFullscreen();
        } else if (video.msRequestFullscreen) {
          video.msRequestFullscreen();
        }
      } else {
        if (document.exitFullscreen) {
          document.exitFullscreen();
        } else if (document.mozCancelFullScreen) {
          document.mozCancelFullScreen();
        } else if (document.webkitExitFullscreen) {
          document.webkitExitFullscreen();
        } else if (document.msExitFullscreen) {
          document.msExitFullscreen();
        }
      }
    }

    // 更改播放速度
    function changeSpeed(speed) {
      const video = document.getElementById('videoPlayer');
      video.playbackRate = speed;
      updateStatus('playing', `正在播放 (${speed}x)`);
    }

    // 切换静音
    function toggleMute() {
      const video = document.getElementById('videoPlayer');
      const volumeSlider = document.getElementById('volumeSlider');
      
      // 保存当前音量,用于恢复
      if (video.volume > 0) {
        video.dataset.previousVolume = video.volume;
      }

      if (video.volume > 0) { // 当前非静音,执行静音操作
        video.volume = 0;
        volumeSlider.value = 0;
        document.getElementById('volumePercentage').textContent = '0%';
      } else { // 当前静音,执行取消静音操作
        const previousVolume = parseFloat(video.dataset.previousVolume || 0.7); // 默认0.7
        video.volume = previousVolume;
        volumeSlider.value = Math.round(previousVolume * 100);
        document.getElementById('volumePercentage').textContent = Math.round(previousVolume * 100) + '%';
      }
      
      // 更新音量图标 (滑块旁的图标)
      const volumeIcon = document.getElementById('volumeIcon');
      if (video.volume === 0) {
        volumeIcon.className = 'fas fa-volume-mute';
      } else if (video.volume < 0.5) {
        volumeIcon.className = 'fas fa-volume-down';
      } else {
        volumeIcon.className = 'fas fa-volume-up';
      }

      updateMuteButtonIcon(); // 更新静音按钮图标
    }

    // 更新静音按钮的图标
    function updateMuteButtonIcon() {
      const video = document.getElementById('videoPlayer');
      const muteButtonIcon = document.getElementById('muteButtonIcon');
      if (muteButtonIcon) {
        if (video.volume === 0) { // 当前已静音
          muteButtonIcon.className = 'fas fa-volume-off'; // 图标显示静音状态
        } else { // 当前非静音
          muteButtonIcon.className = 'fas fa-volume-up'; // 图标显示非静音状态
        }
      }
    }
    
    // 主题切换功能
    document.getElementById('themeToggle').addEventListener('click', function () {
      document.body.classList.toggle('dark-mode');
      localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));

      const icon = this.querySelector('i');
      if (document.body.classList.contains('dark-mode')) {
        icon.className = 'fas fa-sun';
      } else {
        icon.className = 'fas fa-moon';
      }
    });

    // 加载主题设置
    function loadTheme() {
      const darkMode = localStorage.getItem('darkMode') === 'true';
      const themeToggle = document.getElementById('themeToggle');
      const icon = themeToggle.querySelector('i');

      if (darkMode) {
        document.body.classList.add('dark-mode');
        icon.className = 'fas fa-sun';
      } else {
        document.body.classList.remove('dark-mode');
        icon.className = 'fas fa-moon';
      }
    }
  </script>
</body>
</html>



翻译图片
显示原图
翻译为
中文
英文

复制译文

下载图片

点评

超级棒的工具  发表于 2025-6-18 08:38

免费评分

参与人数 29威望 +2 吾爱币 +129 热心值 +24 收起 理由
lijinfeng + 1 谢谢@Thanks!
July8 + 1 + 1 热心回复!
Pipi2018 + 1 谢谢@Thanks!
xuanleaxuan + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
lunker2019 + 1 + 1 用心讨论,共获提升!
ysxc666 + 1 + 1 我很赞同!
Azad + 1 + 1 我很赞同!
xiafengyu + 1 我很赞同!
icydust + 1 + 1 用心讨论,共获提升!
tianya0908 + 1 + 1 用心讨论,共获提升!
俊公子 + 1 + 1 谢谢@Thanks!
房州波哥 + 1 + 1 测试了7.7,优化点:m3u8流增加点播与直播切换并对应
ink-Crytsal + 1 + 1 谢谢@Thanks!
xy6538 + 1 谢谢@Thanks!
_paopao + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
huang9126 + 1 用心讨论,共获提升!
DavisC + 1 + 1 谢谢@Thanks!
nieshi666 + 2 + 1 鼓励转贴优秀软件安全工具和文档!
poi8848328 + 1 + 1 谢谢@Thanks!
luojp52pojie520 + 1 + 1 谢谢@Thanks!
klmytwb + 1 + 1 谢谢@Thanks!
ygq170063 + 2 + 1 谢谢@Thanks!
yixi + 1 + 1 热心回复!
anning666 + 1 + 1 我很赞同!
苏紫方璇 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
某些人 + 1 + 1 谢谢@Thanks!
xueren114 + 1 + 1 用html做还是第一次见23333
shenquanwusheng + 1 热心回复!
withwy + 1 + 1 谢谢@Thanks!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| 小智xyz 发表于 2025-6-24 02:56
服务器部署说明:用服务器部署的话,https部署就播放不了http开头的在线视频,除非有忽略证书的浏览器,例如(X浏览器),https只能播放https开头的视频。 http部署的就https的视频和http开头的视频都可以播放。 这不是bug,修不了的,互联网上所有的在线播放器都有这个问题,除非本地播放。
xiaoyonggaoya 发表于 2025-6-13 22:05
好像有点问题,点那个上传本地视频选的视频可以正常播放,但是在左边那个输入视频 URL 的框输视频直链进去点播放按钮没有反应,直接打开输的视频直链可以正常播放
房州波哥 发表于 2025-7-3 11:20
测试了7.7,
需要优化点:
m3u8流增加点播与直播切换并对应界面:
直播时无需显示进度条,当然无需拖动
点播时,支持拖动进度条

另外:
帖子的代码有问题,不能播放m3u8
房州波哥 发表于 2025-7-3 11:31
另 代码里集成的m3u8测试地址建议使用https开头的,避免https的web服务器加载失败,如下面的地址,

诗酒远方,千里房县   综合台

https://play-hsbj.vzan.com/844270/495391109072307857/live.m3u8
房州波哥 发表于 2025-7-3 16:12

播放界面双进度条,可以去掉一个
房州波哥 发表于 2025-7-4 11:39
https://m.ivendk.com/qlive/

效果参考上面的:直播时,没有进度条

腾讯web视频播放器更牛的功能:pc端看直播自动播放,微信端看直播实时截图作为视频背景。
skzhaixing 发表于 2025-7-6 15:38
小智xyz 发表于 2025-7-6 08:31
集合的m3u8地址有吗,我看一下。

https://wxsj.lanzoub.com/iN8Q130ed62j   这种集合
picoyiyi 发表于 2025-6-14 18:05
直链不行的话还能开发或者更新么?
房州波哥 发表于 2025-7-4 08:47
小智xyz 发表于 2025-7-4 08:38
https://c.wss.pet/f/hel3r8ah5hn
你自己去改就行,我不会改我有这个需求。

还是不对
直播模式还是有进度条,进度条最左最右还是有时间

正常的直播模式没有进度条,最右没有时间,最右一般有两种模式:计时显示观看时长/无时间
房州波哥 发表于 2025-7-4 08:07
小智xyz 发表于 2025-7-4 05:10
这个不会改的,你可以自己改,
你测试一下加了一个点播和直播按钮,无法加上自动检测,只能手动切换,先 ...


感谢感谢,您这下载链接不能下载呢

第342行代码,值改为-1后,把视频里的进度条隐藏了
   
  z-index: -1; /* 确保在视频上方 */  

withwy 发表于 2025-6-13 20:56
这看着不错。
wangsheng518 发表于 2025-6-13 21:32
这个 怎么使用了
zhengzhenhui945 发表于 2025-6-13 21:33
感谢分享,还支持视频链接,雀食不错
 楼主| 小智xyz 发表于 2025-6-13 21:49
吾爱这个代码有问题我复制尝试一下,发现在线链接不能播放,建议去开源链接下载
 楼主| 小智xyz 发表于 2025-6-13 21:55

可以下载附件解压,直接打开html文件使用
 楼主| 小智xyz 发表于 2025-6-13 22:12
xiaoyonggaoya 发表于 2025-6-13 22:05
好像有点问题,点那个上传本地视频选的视频可以正常播放,但是在左边那个输入视频 URL 的框输视频直链进去 ...

他这个只支持直链接视频播放
xiaoyonggaoya 发表于 2025-6-13 22:26
小智xyz 发表于 2025-6-13 22:12
他这个只支持直链接视频播放

是直链也不行,甚至输入框里默认的那个 http://cloud.ruiboyun.net/vod/vod3/e0u83v08/index.m3u8 也不行
 楼主| 小智xyz 发表于 2025-6-13 22:30
xiaoyonggaoya 发表于 2025-6-13 22:26
是直链也不行,甚至输入框里默认的那个 http://cloud.ruiboyun.net/vod/vod3/e0u83v08/index.m3u8 也不行

你试一下这个http://vjs.zencdn.net/v/oceans.mp4
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-2-19 09:39

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表