Cập nhật gần đây
latest

Đầu trang 1 - 728x90

468x60

Slider

latest

Slider Right

randomposts6

Anime Vietsub

Anime Vietsub/block-7

Có thể bạn đã biết

Có thể bạn đã biết/block-2

Top Anime

Top Anime/block-2

Anime Wallpapers

Anime Wallpapers/block-3

Characters

Characters/block-1

Fan Art

Fan Art/block-7

Triết lý Anime/Manga/Light Novel

Những câu nói hay/block-5

Đề cử

Đề cử/block-8

Music

Music/block-9

Blogger Chitose2D

Blogger%20Chitose2D/block-4

Web Novel/Light Novel

Web%20Novel%20-%20Light%20Novel/block-1

AMV

AMV

Mới nhất

Thứ Tư

Code trình phát video HTML5, thế hệ mới 2026

Trong thời đại mà trải nghiệm xem video trên web ngày càng được đặt lên hàng đầu, việc sử dụng những player mặc định của HTML5 đôi khi không còn đủ để đáp ứng nhu cầu về giao diện, hiệu năng và tính tùy biến. Chính vì vậy, mình đã bắt tay vào xây dựng một trình phát video hoàn toàn custom – không chỉ đẹp hơn, mượt hơn mà còn mang lại cảm giác hiện đại như các nền tảng streaming chuyên nghiệp.

Bộ code dưới đây là một phiên bản “player thế hệ mới 2026” với giao diện tối giản nhưng cực kỳ tinh tế, tích hợp hàng loạt tính năng nâng cao như: chuyển tập, đổi chất lượng video linh hoạt, preview khi tua, hiệu ứng seek giống YouTube, fullscreen thông minh (kèm xoay ngang trên mobile), lưu trạng thái xem, và tối ưu hóa trải nghiệm trên cả PC lẫn điện thoại.

Điểm mình tâm đắc nhất không chỉ nằm ở việc nó hoạt động ổn định, mà còn ở cách mọi tương tác đều được xử lý mượt mà, gần như không có độ trễ — từ việc kéo timeline, hover preview cho tới animation UI. Đây không đơn thuần là một đoạn code, mà là một trải nghiệm xem video hoàn chỉnh được xây dựng bằng HTML, CSS và JavaScript thuần.

Trong bài viết này, mình sẽ chia sẻ toàn bộ code cũng như giải thích cách hoạt động của từng phần, để bạn có thể tự tích hợp vào blog hoặc website của mình một cách dễ dàng.

Demo ảnh: Code trình phát video HTML5, thế hệ mới 2026 👇

🚀 Danh sách tính năng nổi bật

Trình phát video này không chỉ dừng lại ở việc “play được video”, mà được xây dựng theo hướng một player hoàn chỉnh như các nền tảng streaming hiện đại. Dưới đây là những tính năng chính:


🎬 Trải nghiệm xem video mượt mà

• Play / Pause nhanh, không delay

• Tự động hiển thị loading khi buffer

• Tối ưu hiệu năng với `will-change`, `transform`, giảm giật lag


⏩ Tua video thông minh (Seek UX nâng cao)

• Double click trái/phải để tua ±10s (giống YouTube)

• Hiệu ứng overlay hiển thị số giây tua cực mượt

• Kéo thanh progress mượt, không giật


🖼 Preview khi rê chuột (PC)

• Hiển thị thumbnail preview theo thời gian

• Canvas render frame video real-time

• Tối ưu chỉ render khi cần → tiết kiệm tài nguyên


📺 Hỗ trợ nhiều tập & nhiều chất lượng

• Chuyển tập nhanh (Episode system)

• Tự động load danh sách quality theo từng tập

• UI dropdown custom (không dùng select mặc định)

• Hiển thị label: SD / HD / FHD / QHD


💾 Ghi nhớ trạng thái người xem

• Lưu thời gian xem theo từng tập (localStorage)

• Reload lại vẫn xem tiếp từ đoạn trước

• Lưu âm lượng người dùng


🔊 Điều khiển âm thanh thông minh

• Slider volume mượt

• Icon thay đổi theo mức âm lượng

• Nhớ lại volume trước khi mute


📱 Tối ưu mobile cực mạnh

• Ẩn volume slider trên mobile

• Gesture touch để tua video

• Fullscreen + xoay ngang (lock orientation)

• UI tự động ẩn/hiện thông minh


🖥 Fullscreen “chuẩn streaming”

• Giao diện fullscreen kiểu YouTube/Netflix

• Gradient + blur nền controls

• Auto-hide controls khi không tương tác

• Hiển thị lại khi rê chuột / chạm


⚙️ Settings panel hiện đại

• Menu settings compact, animation mượt

• Dropdown custom (episode + quality)

• Highlight item đang chọn


⬇️ Tải video trực tiếp

• Nút download video theo quality hiện tại

• Tự động đổi link Dropbox sang chế độ tải


🎨 UI/UX thiết kế hiện đại 2026

• Dark mode chuẩn “true black”

• Button bo góc, hiệu ứng hover mềm mại

• Progress bar gradient + glow

• Animation cubic-bezier mượt như app native


🚫 Bảo vệ & kiểm soát

• Disable chuột phải trên player

• Ẩn control mặc định của video HTML5

• Custom toàn bộ UI

---


👉 Về tổng thể thì đây không chỉ là một video player, mà là một mini streaming system chạy bằng HTML/CSS/JS thuần, có thể dùng trực tiếp cho blog, web phim, hoặc bất kỳ dự án video nào.


Code full bên dưới, làm biếng giải thích vãi, chi tiết bạn có thể tự tìm hiểu trong code, chỗ nào ko hiểu cmt bên dưới nhé!

<div id="playerWrap" style="margin: auto; max-width: 900px; position: relative;">

<style>
#playerWrap *{
  box-sizing:border-box;
  font-family: 'Shantell Sans', sans-serif;
}



#playerWrap video{
width:100%;
background:#000;
display:block;
}

#playerWrap .controls{
  color:#fff;
  padding:12px;
  position:relative;
  z-index:20;   /* 🔥 thêm */
  overflow:visible;
}

.controls{
  position:relative;
}
#playerWrap{
  background:#000 !important;
  position:relative;
  border-radius:16px;
  overflow:hidden;
  box-shadow:
    0 20px 60px rgba(0,0,0,.6),
    0 0 30px rgba(0,0,0,.4),
    inset 0 0 0 1px rgba(255,255,255,.05);
    transform:translateZ(0);   /* thêm */
  will-change:transform;     /* thêm */
}
  
  
/* ===== CONTROL ROW RESPONSIVE ===== */


/* play + time */

#playerWrap #playBtn{
  flex:0 0 auto;
  width:36px !important;
  height:36px !important;
  padding:0;
  border-radius:50%;
}


#playerWrap #time{
white-space:nowrap;
flex:0 0 auto;
font-size:14px;
}



/* ===== BUTTON ===== */

#playerWrap button{
  color:#fff;

  width:40px;
  height:40px;
  padding:0;

  cursor:pointer;

  display:flex;
  align-items:center;
  justify-content:center;
}



/* ===== VOLUME ===== */

#playerWrap #vol{
width:90px;
cursor:pointer;
accent-color:#e33;
}

/* ===== MOBILE ===== */

@media (max-width:600px){

#playerWrap .controls{
padding:10px;
}

#playerWrap #time{
font-size:12px;
}

#playerWrap select{
max-width:110px;
font-size:13px;
padding:5px 6px;
}

#playerWrap #vol{
width:60px;
}

#playerWrap button{
  width:36px;
  height:36px;
}

}

/* ===== PROGRESS ===== */
  
#progress{
  touch-action: pan-x;
}


#played{
  will-change: width;
}


#playerWrap .progress{
  height:8px;
  background:#333;
  margin-top:10px;
  border-radius:4px;
  cursor:pointer;
  position:relative; /* ⭐ BẮT BUỘC */

  will-change:transform; /* thêm dòng này */
}

#playerWrap .progress:hover .played{
  filter:brightness(1.1);
}

#playerWrap .progress:hover{
  box-shadow:0 0 6px rgba(0,0,0,.4);
}

#playerWrap .buffer{
position:absolute;height:100%;width:0;background:#555;
}

#playerWrap .played{
position:absolute;height:100%;width:0;background:#e33;
}
  
#timeBubble{
  position:absolute;
  bottom:14px;
  padding:4px 8px;
  font-size:12px;
  background:#000;
  color:#fff;
  border-radius:6px;
  transform:translateX(-50%) translateY(4px);
  pointer-events:none;
  opacity:0;
  transition:
    opacity .15s ease,
    transform .15s ease;
  white-space:nowrap;
}

#timeBubble.show{
  opacity:1;
  transform:translateX(-50%) translateY(0);
}



/* ===== PREVIEW ===== */

#playerWrap .preview{
position:absolute;
display:none;
width:160px;
height:90px;
background:#000;
border:1px solid #444;
border-radius:6px;
overflow:hidden;
pointer-events:none;
z-index:9;
}

#playerWrap .ptime{
position:absolute;
bottom:0;
left:0;
right:0;
background:rgba(0,0,0,.7);
color:#fff;
font-size:12px;
text-align:center;
}
  
#downloadBtn svg{
  transform: scale(0.88);
  opacity: 0.85;
}
#downloadBtn:hover svg{
  opacity: 1;
}

/* ===== MOBILE: ẨN THANH ÂM LƯỢNG ===== */
@media (max-width:600px){
  #playerWrap #vol{
    display:none;
  }
}


/* ===== SEEK OVERLAY ===== */

.video-wrap{
  position:relative;
}

.video-wrap video{
  width:100%;
  display:block;
}

.video-wrap #seekOverlay{
  position:absolute;
  inset:0;
  pointer-events:none;
  opacity:0;
  transition:opacity .25s ease;
  z-index:5;
}

/* ===== CIRCLE ===== */

#seekOverlay .circle{
  position:absolute;
  top:50%;
  transform: translateY(-50%) scale(.9); /* QUAN TRỌNG */
  width:120px;
  height:120px;
  border-radius:50%;
  background:rgba(0,0,0,.55);

  display:flex;
  align-items:center;
  justify-content:center;
  gap:8px;

  box-shadow:0 0 18px rgba(0,0,0,.6);
  opacity:0;

transition:
  opacity .2s ease-out,
  transform .2s cubic-bezier(.2,.8,.2,1);

  will-change: transform, opacity;
}

#seekOverlay span{
  font-size:22px;
  font-weight:bold;
  color:#fff;
}

/* ===== LEFT ===== */
#seekOverlay.left .circle{
  left:4px;
  opacity:1;
  transform: translateY(-50%) scale(1);
}

/* ===== RIGHT ===== */
#seekOverlay.right .circle{
  right:4px;
  left:auto;
  opacity:1;
  transform: translateY(-50%) scale(1);
}

/* ===== MOBILE ===== */
@media (max-width:600px){
  #seekOverlay .circle{
    width:96px;
    height:96px;
  }

  #seekOverlay span{
    font-size:18px;
  }
}
#centerPlay{
  position:absolute;
  inset:0;
  display:flex;
  align-items:center;
  justify-content:center;
  z-index:6;

  opacity:1;
  transition:
    opacity .25s ease,
    background .25s ease;
}


#centerPlay svg{
  width:72px;
  height:72px;  display:block;

  filter:drop-shadow(0 0 10px rgba(0,0,0,.6));
  cursor:pointer;

  transform:scale(1);
  transition:
    transform .2s cubic-bezier(.2,.8,.2,1),
    opacity .2s ease;
}

/* 👉 khi click → đổi icon */
#centerPlay.playing svg{
  transform:scale(.85);
}

/* 👉 fade out */
#centerPlay.hide{
  opacity:0;
  pointer-events:none;
}
#loadingIcon{
  position:absolute;
  inset:0;
  margin:auto;
  width:64px;
  height:64px;
  border:6px solid rgba(255,255,255,.25);
  border-top-color:#fff;
  border-radius:50%;
  animation:spin .9s linear infinite;
  display:none;
  pointer-events:none; 
  z-index:7;        /* cao hơn centerPlay */

}

@keyframes spin{
  to{transform:rotate(360deg)}
}
  

#video{
  width:100%;
  height:auto;
  max-width:100%;
  max-height:100%;
  object-fit:contain;
  background:black;

  will-change: transform;
}



#playerWrap:fullscreen{
  position:fixed;
  inset:0;
  width:100%;
  height:100%;
  background:#000;

  display:flex;
  flex-direction:column;
}

#playerWrap:fullscreen{
  overflow:hidden;
}

  
#playerWrap:fullscreen .settings-menu{
  bottom:70px;
}
#playerWrap:fullscreen .video-wrap{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
}

#playerWrap:fullscreen video{
  width:100%;
  height:100%;
  max-width:100%;
  max-height:100%;
  object-fit:contain;
  background:#000;
}

@supports (-webkit-touch-callout: none) {
  #playerWrap:fullscreen video{
    object-fit:contain;
    transform: translateZ(0);
  }
}

#playerWrap:fullscreen .controls{
  position:absolute;
  left:0;
  right:0;
  bottom:0;
  z-index:20;
}


#playerWrap:fullscreen .progress{
  position:relative;
  margin-top:10px;   /* giống lúc chưa fullscreen */
}

  /* khi JS add show-controls → HIỆN CẢ HAI */
  #playerWrap.show-controls:fullscreen .controls,
  #playerWrap.show-controls:fullscreen .progress{
    opacity:1;
    pointer-events:auto;
  }

video::-webkit-media-controls {
  display: none !important;
}

video::-webkit-media-controls-enclosure {
  display: none !important;
}

#playerWrap:fullscreen .controls,
#playerWrap:fullscreen .progress{
  opacity:0;
  pointer-events:auto;
  transition: opacity .6s ease;
}

#playerWrap.show-controls:fullscreen .controls,
#playerWrap.show-controls:fullscreen .progress{
  opacity:1;
  pointer-events:auto;
}
#playerWrap:fullscreen .controls{
  transform: translateY(10px);
  transition:
    opacity .6s ease,
    transform .6s ease;
}

#playerWrap.show-controls:fullscreen .controls{
  transform: translateY(0);
}


#playerWrap:fullscreen .preview,
#playerWrap:fullscreen #timeBubble{
  z-index: 30;
}



/* show */
.settings.open .settings-menu{
  display:flex;
}


/* icon only cho phải */
#playerWrap .right-controls button{
  width:36px;
  height:36px;
  padding:0;
}
@media (max-width:600px){

  #playerWrap #time{
    font-size:11px;
  }



  #playerWrap .right-controls button{
    width:32px;
    height:32px;
  }


}

#settingsBtn .gear{
  width:18px;
  height:18px;
  display:block;
  transform-origin:center;
  transition: transform .4s cubic-bezier(.2,.8,.2,1);
}

/* mở menu → xoay nhẹ */
.settings.open #settingsBtn .gear{
  transform: rotate(90deg);
}



@keyframes gear-spin{
  to{ transform: rotate(360deg); }
}


/* ===== FIX CỨNG LUÔN 1 HÀNG ===== */

#playerWrap .row{
  display:flex !important;
  align-items:center !important;
  flex-wrap:nowrap !important;
  margin-right:0px;
    margin-left:0px;
    gap:6px; /* 🔥 thêm dòng này */

}

#playerWrap #playBtn,
#playerWrap #time{
  flex:0 0 auto !important;
}

#playerWrap .right-controls{
  display:flex !important;
  align-items:center;
  gap:6px;
  margin-left:auto;
  flex:0 0 auto !important;
  white-space:nowrap;
}

#playerWrap .controls{
  overflow:visible;
}


/* ===== MODERN SELECT BOX (EP + QUALITY) ===== */

#playerWrap .select-wrap{
  position:relative;
  display:inline-block;
}

#playerWrap select{
  background:rgba(255,255,255,.06);
  color:rgba(255,255,255,.85); /* đổi dòng này */
  border:1px solid rgba(255,255,255,.08);

  padding:5px 28px 5px 10px;
  border-radius:8px;
  font-size:11px;
}



/* hover */
#playerWrap select:hover{
  background:rgba(255,255,255,.1);
}


/* focus */
#playerWrap select:focus{
  outline:none;
  border-color:rgba(255,255,255,0.25);
  box-shadow:0 0 0 3px rgba(255,255,255,0.08);
}

/* caret mới */


#playerWrap .select-wrap:hover::after{
  opacity:.9;
}

/* dropdown option */
#playerWrap select option{
  background:#111;
  color:#fff;
}
#playerWrap select option:checked{
  background:#e50914;
  color:#fff;
}


/* ============================= */
/* 🔥 FULLSCREEN – STYLE YOUTUBE */
/* ============================= */

#playerWrap:fullscreen .controls{
  position:absolute;
  left:0;
  right:0;
  bottom:0;

  padding:20px 24px 18px;

  background:linear-gradient(
    to top,
    rgba(0,0,0,.85) 0%,
    rgba(0,0,0,.65) 40%,
    rgba(0,0,0,.0) 100%
  );

  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);

  border:none;
}

/* progress bar full width + sát đáy */
#playerWrap:fullscreen .progress{
  position:absolute;
  left:0;
  right:0;
  bottom:72px;

  margin:0 24px;
  height:4px;
  border-radius:2px;
  background:rgba(255,255,255,.25);
}

/* played đỏ kiểu youtube */




/* controls icon sáng hơn */
#playerWrap:fullscreen button{
  background:transparent;
  border:none;
}

#playerWrap:fullscreen button:hover{
  background:rgba(255,255,255,.15);
}

/* select box trong suốt hơn */
#playerWrap:fullscreen select{
  background:rgba(255,255,255,.1);
  border:1px solid rgba(255,255,255,.15);
  color:rgba(255,255,255,.85); /* thêm dòng này */
}


/* time chữ sáng hơn */
#playerWrap:fullscreen #time{
  opacity:.9;
  font-size:14px;
}
/* ===== PROGRESS HOVER EFFECT (NO LAYOUT SHIFT) ===== */

#playerWrap .progress::before{
  content:"";
  position:absolute;
  left:0;
  right:0;
  top:50%;
  height:100%;
  border-radius:inherit;
  background:inherit;
  transform:translateY(-50%) scaleY(1);
  transition:transform .2s ease;
  pointer-events:none;
}

#playerWrap .progress:hover::before{
  transform:translateY(-50%) scaleY(1.4);
}

/* ===================================== */
/* 🎬 MODERN STREAMING UI 2026 */
/* ===================================== */

/* ===== CENTER PLAY ===== */
#centerPlay{
  background:transparent;
  backdrop-filter:none;
  -webkit-backdrop-filter:none;
}

#playerWrap.playing #centerPlay{
  background:rgba(0,0,0,.35);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

#centerPlay svg{
  width:84px;
  height:84px;

  background:rgba(0,0,0,.5);
  backdrop-filter:blur(8px);

  border-radius:50%;
  padding:22px;

  box-shadow:0 10px 35px rgba(0,0,0,.5);

  transition:transform .25s ease;
}

#centerPlay:hover svg{
  transform:scale(1.1);
  opacity:.95;
}

/* ===== BUTTON STYLE MODERN ===== */
#playerWrap button{
  background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.12);
  border-radius:14px;

  transition:
    background .25s ease,
    box-shadow .25s ease,
    border .25s ease;
}

#playerWrap button:hover{
  background:rgba(255,255,255,.14);
  box-shadow:0 4px 12px rgba(0,0,0,.25);
}

/* ===== PLAY BUTTON LEFT ===== */
#playBtn{
  border-radius:50% !important;
  background:rgba(255,255,255,.12);
}



/* ===== VOLUME SLIDER MODERN ===== */
#vol{
  height:4px;
  border-radius:4px;
  background:linear-gradient(to right,#e50914,#ff4d4d);
  accent-color:#e50914;
}

/* ===== PROGRESS BAR PREMIUM ===== */


#playerWrap .buffer{
  background:rgba(255,255,255,.35);
}

#playerWrap .played{
  background:linear-gradient(
    90deg,
    #e50914,
    #ff2d55
  );
}

/* ===== PROGRESS GLOW ===== */
#playerWrap .played::after{
  content:"";
  position:absolute;
  right:-4px;
  top:50%;
  transform:translateY(-50%);
  width:12px;
  height:12px;
  border-radius:50%;
  background:#fff;
  box-shadow:
        0 0 12px rgba(255,255,255,.9),
    0 0 25px rgba(229,9,20,.8);
}

/* ===== TIME STYLE ===== */
#time{
  font-weight:500;
  letter-spacing:.3px;
  opacity:.85;
}



/* ===== FORCE TRUE BLACK ===== */


#playerWrap .controls{
  background:
    linear-gradient(
      to top,
      rgba(0,0,0,.95) 0%,
      rgba(0,0,0,.85) 50%,
      rgba(0,0,0,0) 100%
    );
}


.adropdown{
  position:relative;
  margin-bottom:0;
  z-index:1;              /* ✅ thêm dòng này */
}

.adropdown.open{
  z-index:999;            /* ✅ thêm khối này */
}


/* ===== ARROW ===== */


 #playerWrap.error-state::after{
  content:"Không thể tải video";
  position:absolute;
  inset:0;
  display:flex;
  align-items:center;
  justify-content:center;
  color:white;
  font-size:16px;
  background:rgba(0,0,0,.7);
  z-index:50;
}



/* ============================== */
/* 🎛 SETTINGS PANEL – COMPACT 2026 */
/* ============================== */

.settings{
  position:relative;
}

/* PANEL */
.settings-menu{
  position:absolute;
  bottom:54px;
  right:0;

  width:140px;                 /* nhỏ hơn */
  max-width:80vw;

  background:rgba(22,22,22,.96);
  backdrop-filter:none;
  -webkit-backdrop-filter:none;


  border:1px solid rgba(255,255,255,.06);
  border-radius:14px;

  padding:10px 10px 12px;
  display:none;
  flex-direction:column;
  gap:10px;
  box-shadow:
    0 25px 60px rgba(0,0,0,.75),
    0 0 0 1px rgba(255,255,255,.05);

  animation:settingsIn .16s ease;
  z-index:200;
}

.settings.open .settings-menu{
  display:flex;
}

@keyframes settingsIn{
  from{
    opacity:0;
    transform:translateY(8px);
  }
  to{
    opacity:1;
    transform:translateY(0);
  }
}

/* LABEL */
.settings-menu label{
  font-size:10px;  text-align:center;

  font-weight:600;
  letter-spacing:.6px;
  color:rgba(255,255,255,.4);
  margin-bottom:4px;
}

/* ============================== */
/* 🎬 DROPDOWN COMPACT */
/* ============================== */

.adropdown-selected{
  background:rgba(255,255,255,.05);
  border:1px solid rgba(255,255,255,.08);
  border-radius:10px;

  padding:8px 10px;
  font-size:12px;

  display:flex;
  align-items:center;
  justify-content:space-between;

  cursor:pointer;
  transition:.15s ease;
}

.adropdown-selected:hover{
  background:rgba(255,255,255,.12);
}

/* arrow nhỏ hơn */
.adropdown-selected::after{
  content:"";
  width:6px;
  height:6px;
  border-right:2px solid rgba(255,255,255,.6);
  border-bottom:2px solid rgba(255,255,255,.6);
  transform:rotate(45deg);
  transition:.15s ease;
}

.adropdown.open .adropdown-selected::after{
  transform:rotate(-135deg);
}

/* dropdown list */
.adropdown-menu{
  position:absolute;
  top:100%;
  left:0;
  right:0;

  background:rgba(24,24,24,.98);
  border:1px solid rgba(255,255,255,.06);
  border-radius:10px;

  max-height:90px;      /* thấp hơn */
  overflow-y:auto;

  padding:4px;

  display:none;

  box-shadow:0 14px 30px rgba(0,0,0,.55);
}

.adropdown.open .adropdown-menu{
  display:block;
}

/* item */
.adropdown-menu div{
  padding:6px 4px;
  border-radius:8px;
  font-size:12px;
  cursor:pointer;
  transition:.15s ease;
}
.q-label{
  font-size:9px;          /* nhỏ hơn */
  margin-left:4px;
  padding:3px 3px 2px 3px;        /* bọc nhỏ lại */
  border-radius:3px;
  color: #000;
  background: #eee; /* Màu nền */
  line-height:1;
}

.q-label.FHD{
  background:#CC3333;color: #fff;
}
.q-label.QHD{
  background:#CC3333;color: #fff;
}
@media(max-width:600px){

  .settings-menu{
    width:125px;        /* nhỏ lại */
    padding:7px;
    gap:7px;
    border-radius:12px;
  }

  .settings-menu label{
    font-size:9px;
  }

  .adropdown-selected{
    padding:6px 7px;
    font-size:10px;
  }

  .adropdown-menu{
    max-height:90px;
  }

}

/* ===== COMPACT SCROLLBAR ===== */

/* Chrome, Edge, Safari */
.adropdown-menu::-webkit-scrollbar,
.settings-menu::-webkit-scrollbar{
  width:6px;
}

.adropdown-menu::-webkit-scrollbar-track,
.settings-menu::-webkit-scrollbar-track{
  background:transparent;
}

.adropdown-menu::-webkit-scrollbar-thumb,
.settings-menu::-webkit-scrollbar-thumb{
  background:rgba(255,255,255,.25);
  border-radius:10px;
}

.adropdown-menu::-webkit-scrollbar-thumb:hover,
.settings-menu::-webkit-scrollbar-thumb:hover{
  background:rgba(255,255,255,.45);
}

/* Firefox */
.adropdown-menu,
.settings-menu{
  scrollbar-width:thin;
  scrollbar-color: rgba(255,255,255,.3) transparent;
}
.adropdown-menu div{
  transition: background .15s ease;
}

.adropdown-menu div:hover{
  background:rgba(255,255,255,.12);
  color:#fff;
}
  
/* ===== EPISODE ACTIVE ICON ===== */

.adropdown-menu div{
  display:flex;
  align-items:center;
  justify-content:space-between;
}

.adropdown-menu div.active::after{
  content:"";
  display:inline-block;
  width:18px;
  height:18px;
  margin-left:6px;

  background-color:#ccc;

  -webkit-mask: url("data:image/svg+xml;utf8,\
  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'>\
  <path fill='white' d='M12 5c7 0 10 7 10 7s-3 7-10 7S2 12 2 12s3-7 10-7zm0 2C7.5 7 5 12 5 12s2.5 5 7 5 7-5 7-5-2.5-5-7-5zm0 2.5A2.5 2.5 0 1 1 9.5 12 2.5 2.5 0 0 1 12 9.5z'/>\
  </svg>") no-repeat center;

  mask: url("data:image/svg+xml;utf8,\
  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'>\
  <path fill='white' d='M12 5c7 0 10 7 10 7s-3 7-10 7S2 12 2 12s3-7 10-7zm0 2C7.5 7 5 12 5 12s2.5 5 7 5 7-5 7-5-2.5-5-7-5zm0 2.5A2.5 2.5 0 1 1 9.5 12 2.5 2.5 0 0 1 12 9.5z'/>\
  </svg>") no-repeat center;

  -webkit-mask-size:contain;
  mask-size:contain;
}
</style>

<div class="video-wrap">
  <video id="video" playsinline="" poster="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjX8GlOt0ZY2Z_PxMi5-3nlARk-Wg6Pk9xwf4WLNXg0BY0t2XGAtLE1Nx5zWcXbiyfLYfIPnkhc5Me0_XnoTFFXIOJxFj6FhSnXDYsObRHxA0igovwYvar1IK5-wdm9N5FmMYsRb3BcNdVxAcENvhCKEzHWH3wC_84O2JYNTXPCm-ppHSjbsMJ0RdluytE/s16000/1.jpg" preload="none">
    
    <!-- ❌ KHÔNG src lúc đầu -->
    <source id="src" type="video/mp4"></source>
  </video>


  <!--✅ NÚT PLAY GIỮA-->
<div id="centerPlay">
  <svg fill="white" id="centerPlayIcon" viewbox="0 0 24 24">
    <polygon points="6,4 20,12 6,20">
  </polygon></svg>
  

  
</div>   <!--LOADING chỉ dùng khi đổi source-->
  <div id="loadingIcon"></div>

  <div id="seekOverlay"></div>
</div>


  



<div class="preview" id="preview">
<span style="font-family: Shantell Sans;"><canvas height="90" id="pcanvas" width="160"></canvas>
</span><div class="ptime" id="ptime"></div>
</div>

<div class="controls">
<div class="row">

<button aria-label="Play" id="playBtn">
<svg fill="white" height="18" id="playIcon" viewbox="0 0 24 24" width="18">
<polygon points="6,4 20,12 6,20">
</polygon></svg>
</button>

<span id="time">0:00 / 0:00</span>

<div class="right-controls">

<button aria-label="Mute" id="muteBtn">
<svg fill="white" height="18" id="volIcon" viewbox="0 0 24 24" width="18">
<path d="M5 9v6h4l5 5V4L9 9H5z">
<path d="M16.5 12c0-1.8-1-3.3-2.5-4.1v8.2c1.5-.8 2.5-2.3 2.5-4.1z">
<path d="M14 3.2v2.1c2.9 1 5 3.8 5 6.7s-2.1 5.7-5 6.7v2.1c4-.9 7-4.5 7-8.8s-3-7.9-7-8.8z">
</path></path></path></svg>
</button>




<input id="vol" type="range" min="0" max="1" step="0.01" value="1">

  
<button id="downloadBtn" title="Tải video">
  <svg width="20" height="20" viewBox="0 0 43.329 43.329" fill="white">
    <path d="M18.689 30.359c1.645 1.639 4.305 1.639 5.951 0l14.082-14.014c.91-.906 1.186-2.287.695-3.474-.49-1.187-1.646-1.976-2.932-1.976h-5.879V3.196C30.606 1.445 29.17 0 27.419 0H15.908C14.156 0 12.7 1.444 12.7 3.196v7.698H6.842c-1.284 0-2.441.79-2.931 1.976-.49 1.187-.216 2.561.694 3.466l14.084 14.023z"/>
    <path d="M39.485 34.376H3.842c-1.751 0-3.171 1.419-3.171 3.171v2.611c0 1.751 1.42 3.171 3.171 3.171h35.645c1.751 0 3.171-1.42 3.171-3.171v-2.611c0-1.752-1.42-3.171-3.173-3.171z"/>
  </svg>
</button>


<button id="fsBtn" title="Toàn màn hình">
  <svg fill="white" height="18" id="fsIcon" viewbox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg">
    <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z">
  </path></svg>
</button>

<div class="settings">
<button aria-label="Settings" class="settings-btn" id="settingsBtn">
<svg class="gear" fill="currentColor" viewbox="0 0 262.39 262.39" xmlns="http://www.w3.org/2000/svg">

  <path d="M245.63,103.39h-9.91c-2.486-9.371-6.197-18.242-10.955-26.432
  l7.015-7.015c6.546-6.546,6.546-17.159,0-23.705l-15.621-15.621
  c-6.546-6.546-17.159-6.546-23.705,0l-7.015,7.015
  c-8.19-4.758-17.061-8.468-26.432-10.955v-9.914
  C159.007,7.505,151.502,0,142.244,0h-22.091
  c-9.258,0-16.763,7.505-16.763,16.763v9.914
  c-9.37,2.486-18.242,6.197-26.431,10.954l-7.016-7.015
  c-6.546-6.546-17.159-6.546-23.705,0.001L30.618,46.238
  c-6.546,6.546-6.546,17.159,0,23.705l7.014,7.014
  c-4.758,8.19-8.469,17.062-10.955,26.433h-9.914
  c-9.257,0-16.762,7.505-16.762,16.763v22.09
  c0,9.258,7.505,16.763,16.762,16.763h9.914
  c2.487,9.371,6.198,18.243,10.956,26.433l-7.015,7.015
  c-6.546,6.546-6.546,17.159,0,23.705l15.621,15.621
  c6.546,6.546,17.159,6.546,23.705,0l7.016-7.016
  c8.189,4.758,17.061,8.469,26.431,10.955v9.913
  c0,9.258,7.505,16.763,16.763,16.763h22.091
  c9.258,0,16.763-7.505,16.763-16.763v-9.913
  c9.371-2.487,18.242-6.198,26.432-10.956l7.016,7.017
  c6.546,6.546,17.159,6.546,23.705,0l15.621-15.621
  c3.145-3.144,4.91-7.407,4.91-11.853s-1.766-8.709-4.91-11.853
  l-7.016-7.016c4.758-8.189,8.468-17.062,10.955-26.432h9.91
  c9.258,0,16.763-7.505,16.763-16.763v-22.09
  C262.393,110.895,254.888,103.39,245.63,103.39z
  M131.198,191.194c-33.083,0-59.998-26.915-59.998-59.997
  c0-33.083,26.915-59.998,59.998-59.998
  s59.998,26.915,59.998,59.998
  C191.196,164.279,164.281,191.194,131.198,191.194z">

  <path d="M131.198,101.199
  c-16.541,0-29.998,13.457-29.998,29.998
  c0,16.54,13.457,29.997,29.998,29.997
  s29.998-13.457,29.998-29.997
  C161.196,114.656,147.739,101.199,131.198,101.199z">
</path></path></svg>

</button>


  <div class="settings-menu">

  <label>Chọn video</label>
<div class="adropdown" id="episodeDropdown">
  <div class="adropdown-selected"># 01</div>
  <div class="adropdown-menu"></div>
</div>


  <label>Chất lượng</label>
  <div class="adropdown" id="qualityDropdown">
    <div class="adropdown-selected"></div>
    <div class="adropdown-menu"></div>
  </div>
<select id="episode" style="display: none;"></select>
<select id="quality" style="display: none;"></select>
</div>

</div>

<div class="settings-backdrop"></div>


</div>
</div>


<div class="progress" id="progress" style="font-family: Saira;">
<div class="buffer" id="buffer"></div>
<div class="played" id="played"></div>
  <!--🔹 TIME BUBBLE KHI KÉO-->
  <div id="timeBubble">0:00</div>
</div>
</div>
</div>

<script>
  
/* ===== BIẾN DÙNG CHUNG – DÁN NGAY ĐẦU ===== */
let hideTimer = null;
let interacting = false;
let isDragging = false;
let isOverControls = false; // 👈 THÊM


/* ======================================== */
  
function fmt(t){
  if (!isFinite(t)) return "0:00";

  t = Math.max(0, t);
  const m = Math.floor(t / 60);
  const s = Math.floor(t % 60);
  return `${m}:${s < 10 ? "0"+s : s}`;
}




 /* ===== EPISODES DATA ===== */

const EPISODES = {
  ep001: {
    360: "https://dl.dropboxusercontent.com/scl/fi/xi55bn2t9yx73f5x8vszr/AMV-360p-SD-Chitose2D-MEP-AMV-Nh-ng-c-i-ch-t-kh-qu-n-nh-t-trong-Anime-Vietsub-Ph-n-1.mp4?rlkey=5j6w1suk8gdt70p7uy3116azm&e=1&st=8j1k97dq&dl=0",
    720: "https://dl.dropboxusercontent.com/scl/fi/gmucexatfe619qpiqxjzm/AMV-720p-HD-Chitose2D-MEP-AMV-Nh-ng-c-i-ch-t-kh-qu-n-nh-t-trong-Anime-Vietsub-Ph-n-1.mp4?rlkey=jo2xl3e5u6zmxuzngvcnzomc5&e=1&st=x0st4srq&dl=0" },

  
  ep002: {
    360: "https://dl.dropboxusercontent.com/scl/fi/hmbiflm2zt2sub9oi83un/AMV-360p-SD-Chitose2D-MEP-AMV-Nh-ng-c-i-ch-t-kh-qu-n-nh-t-trong-Anime-Vietsub-Ph-n-2.mp4?rlkey=jc86bolqntbufaz9kphmni92t&e=3&st=8yn7b8xu&dl=0",
    720: "https://dl.dropboxusercontent.com/scl/fi/kgwyon7xcbx4c8wbm6uy8/AMV-720p-HD-Chitose2D-MEP-AMV-Nh-ng-c-i-ch-t-kh-qu-n-nh-t-trong-Anime-Vietsub-Ph-n-2.mp4?rlkey=rc3oair8w1s7ze8ptnrowf1k8&e=1&st=69lefmcm&dl=0" },
    
  ep003: {
    360: "https://dl.dropboxusercontent.com/scl/fi/0s9y10nyfr9djjpxglwdp/AMV-360p-SD-Chitose2D-MEP-AMV-Nh-ng-c-i-ch-t-kh-qu-n-nh-t-trong-Anime-Vietsub-Ph-n-3.mp4?rlkey=fzk88j947sbzonhohz2j0brdq&e=1&st=d1yy6a37&dl=0",
    720: "https://dl.dropboxusercontent.com/scl/fi/m3wwrjdec8bvzuyftbb5o/AMV-720p-HD-Chitose2D-MEP-AMV-Nh-ng-c-i-ch-t-kh-qu-n-nh-t-trong-Anime-Vietsub-Ph-n-3.mp4?rlkey=4u2ulf5z6dhq2rp7zy35q63vm&e=1&st=a26w29ul&dl=0" },

};

 
function getPreviewSrc(ep){
  
  const sources = EPISODES[ep];
  return sources[360] || Object.values(sources)[0];
}
function updateQualityOptions(ep){
  const sources = EPISODES[ep];
  const current = quality.value;

  quality.innerHTML = "";

  Object.keys(sources)
    .sort((a,b)=>a-b)
    .forEach(q=>{
      const opt = document.createElement("option");
      opt.value = q;
      opt.textContent = q + "p";
      quality.appendChild(opt);
    });

  updateQualityDropdownUI();
}


// 🔥 DÁN NGAY DƯỚI ĐÂY
const qualityDropdown = document.getElementById("qualityDropdown");
const qualitySelected = qualityDropdown.querySelector(".adropdown-selected");

qualitySelected.onclick = (e)=>{
  e.stopPropagation();

  document.querySelectorAll(".adropdown").forEach(d=>{
    if(d !== qualityDropdown) d.classList.remove("open");
  });

  qualityDropdown.classList.toggle("open");
};



const TOTAL_EP = Object.keys(EPISODES).length;
// Ẩn chọn tập nếu chỉ có 1 tập
if (TOTAL_EP <= 1) {
  const settingsMenu = document.querySelector(".settings-menu");
  const labels = settingsMenu.querySelectorAll("label");

  labels.forEach(label => {
    if (label.textContent.trim() === "Chọn tập") {
      label.style.display = "none";
    }
  });

  document.getElementById("episodeDropdown").style.display = "none";
}
 
const playerWrap = document.getElementById("playerWrap");
  playerWrap.addEventListener("contextmenu", e => {
  e.preventDefault();
});
function arrowSVG(dir){
  return `
  <svg width="32" height="32" viewBox="0 0 24 24" fill="white"
       style="opacity:.9;transform:${dir==="left"?"rotate(180deg)":"none"}">
    <path d="M8 5v14l11-7z"/>
  </svg>`;
}

function getBaseSrc(){
  const ep = episode.value;
  const sources = EPISODES[ep];
  return sources[360] || Object.values(sources)[0];
}


const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);

const video=playerWrap.querySelector("#video");
const centerPlay = document.getElementById("centerPlay");
const centerPlayIcon = document.getElementById("centerPlayIcon");

const src=playerWrap.querySelector("#src");
const playBtn=playerWrap.querySelector("#playBtn");
const progress=playerWrap.querySelector("#progress");
const controls = playerWrap.querySelector(".controls");

/* ===== GIỮ CONTROLS KHI ĐANG TƯƠNG TÁC ===== */

// PC: hover vào controls / progress → KHÔNG ẨN
[controls, progress].forEach(el=>{
  el.addEventListener("mouseenter", ()=>{
    interacting = true;
    isOverControls = true;
    clearTimeout(hideTimer);
    showControls();
  });

el.addEventListener("mouseleave", ()=>{
  if (isDragging) return; // 👈 CỐT LÕI
  interacting = false;
  isOverControls = false;
  scheduleHide();
});

});


const timeBubble = playerWrap.querySelector("#timeBubble");
const loadingIcon = document.getElementById("loadingIcon");
video.addEventListener("error", () => {
  loadingIcon.style.display = "none";
  playerWrap.classList.add("error-state");
});

video.addEventListener("waiting", () => {
  if (!video.paused) {
    loadingIcon.style.display = "flex";
  }
});

video.addEventListener("playing", () => {
  loadingIcon.style.display = "none";
});

const episode = playerWrap.querySelector("#episode");

const played=playerWrap.querySelector("#played");
const buffer=playerWrap.querySelector("#buffer");
const timeEl = playerWrap.querySelector("#time");
  

progress.addEventListener("touchend", ()=>{
  isDragging = false;
  video.currentTime = dragTime;

  clearTimeout(hideTimer);
  hideTimer = setTimeout(() => {
    playerWrap.classList.remove("show-controls");
    interacting = false;
  }, 2000);
}, { passive:false });




progress.addEventListener("touchmove", e=>{
  isDragging = true;
  interacting = true;
  clearTimeout(hideTimer);
  updateDrag(e.touches[0].clientX);
},{passive:true});


  
  
  
let dragTime = 0;

function updateDrag(clientX){
  if (!video.duration) return;

  const rect = progress.getBoundingClientRect();
  const x = Math.min(Math.max(clientX - rect.left, 0), rect.width);
  const percent = x / rect.width;

  dragTime = percent * video.duration;
  played.style.width = `${percent * 100}%`;
  timeEl.textContent = `${fmt(dragTime)} / ${fmt(video.duration)}`;
  // 👉 THÊM DÒNG NÀY
  if (!isMobile) updatePreview(clientX);
}


progress.addEventListener("mousedown", e=>{
  isDragging = true;
  interacting = true;        // 👈 THIẾU
  clearTimeout(hideTimer);   // 👈 THIẾU
  updateDrag(e.clientX);
});


document.addEventListener("mousemove", e=>{
  if(isDragging) updateDrag(e.clientX);
});

document.addEventListener("mouseup", (e) => {
  if (!isDragging) return;

  isDragging = false;
  video.currentTime = dragTime;

  // 🎯 kiểm tra phần tử ngay dưới con trỏ
  const el = document.elementFromPoint(e.clientX, e.clientY);

  const overControls =
    el &&
    (controls.contains(el) || progress.contains(el));

  if (overControls) {
    interacting = true;
    isOverControls = true;
    clearTimeout(hideTimer);
    showControls();
  } else {
    interacting = false;
    isOverControls = false;
    scheduleHide();
  }
});





const quality=playerWrap.querySelector("#quality");
const vol = playerWrap.querySelector("#vol");
const muteBtn = playerWrap.querySelector("#muteBtn");
const volIcon = playerWrap.querySelector("#volIcon");

const VOL_KEY = "player_volume";

const preview=playerWrap.querySelector("#preview");
const settings = playerWrap.querySelector(".settings");

const pcanvas=playerWrap.querySelector("#pcanvas");
const ptime=playerWrap.querySelector("#ptime");
const ctx=pcanvas.getContext("2d");
const vHidden=document.createElement("video");
  


  
vHidden.muted=true;
vHidden.playsInline=true;
vHidden.preload="auto";
let pendingDraw = false;

vHidden.addEventListener("seeked", ()=>{
  if(!pendingDraw) return;
  try{
    ctx.drawImage(vHidden, 0, 0, 160, 90);
  }catch(e){}
  pendingDraw = false;
});

function seekSafe(delta){
  if(!video.duration) return;

  const next = Math.min(
    Math.max(video.currentTime + delta, 0),
    video.duration - 0.05
  );

  video.currentTime = next;
}

/* ===== SVG VOLUME ICON ===== */

function updateMuteIcon(){
if(video.muted || video.volume == 0){
volIcon.innerHTML = `
<path d="M3 10v4h4l5 5V5L7 10H3z"/>
<line x1="16" y1="8" x2="22" y2="16" stroke="white" stroke-width="2"/>
<line x1="22" y1="8" x2="16" y2="16" stroke="white" stroke-width="2"/>
`;
}
else if(video.volume < 0.5){
volIcon.innerHTML = `
<path d="M3 10v4h4l5 5V5L7 10H3z"/>
<path d="M16 9c1.5 1.5 1.5 4.5 0 6" stroke="white" stroke-width="2" fill="none"/>
`;
}
else{
volIcon.innerHTML = `
<path d="M3 10v4h4l5 5V5L7 10H3z"/>
<path d="M16 8c2 2 2 6 0 8" stroke="white" stroke-width="2" fill="none"/>
<path d="M19 5c3 3 3 11 0 14" stroke="white" stroke-width="2" fill="none"/>
`;
}
}






/* ===== PLAY / PAUSE ===== */

playBtn.onclick = (e) => {
  e.stopPropagation();

  if (!src.src) {   // 🔥 đổi ở đây
    loadingIcon.style.display = "flex";
loadVideo(true);

    return;
  }

  if (video.paused) {
    video.play();
  } else {
    video.pause();
  }
};


const playIcon = playerWrap.querySelector("#playIcon");

let centerHideTimer = null;

video.addEventListener("play", () => {

  playIcon.innerHTML = `
    <rect x="6" y="4" width="4" height="16"/>
    <rect x="14" y="4" width="4" height="16"/>
  `;

  centerPlayIcon.innerHTML = `
    <rect x="6" y="4" width="4" height="16"/>
    <rect x="14" y="4" width="4" height="16"/>
  `;

  centerPlay.classList.remove("hide");

  clearTimeout(centerHideTimer);

  centerHideTimer = setTimeout(() => {
    centerPlay.classList.add("hide");
  }, 600);

});



/* ===== PROGRESS ===== */



/* ===== BUFFER ===== */

video.onprogress=()=>{
if(video.buffered.length && video.duration){
buffer.style.width =
(video.buffered.end(video.buffered.length-1)/video.duration*100)+"%";
}
};





/* ===== QUALITY SWITCH ===== */

function getResumeKey(ep){
  return `resume_${ep}`;
}


function loadVideo(forcePlay = false){
  const ep = episode.value;

  if(!EPISODES[ep][quality.value]){
    quality.value = Object.keys(EPISODES[ep])[0];
  }

  const q  = quality.value;
  const srcUrl = EPISODES[ep][q];
  if(!srcUrl) return;

  const resumeKey = getResumeKey(ep);
  const savedTime = +localStorage.getItem(resumeKey) || 0;

const shouldAutoplay = forcePlay || !video.paused;

  src.src = srcUrl;
  video.load();

  vHidden.src = getPreviewSrc(ep);
  vHidden.currentTime = 0;
  pendingDraw = false;

  video.onloadedmetadata = ()=>{
    if(savedTime > 1 && savedTime < video.duration - 2){
      video.currentTime = savedTime;
    }

    // 👇 CHỈ autoplay nếu thực sự đang play
    if(shouldAutoplay){
      video.play().catch(()=>{});
    }
  };
}



// lưu time ĐÚNG theo tập + quality
video.addEventListener("timeupdate", ()=>{
  if(isDragging) return; // ⛔ đang kéo thì bỏ qua

  localStorage.setItem(
    getResumeKey(episode.value),
    video.currentTime
  );

if (!video.duration) return;
const p = video.currentTime / video.duration;


  played.style.width = `${p*100}%`;
  timeEl.textContent = `${fmt(video.currentTime)} / ${fmt(video.duration)}`;
});

quality.onchange = () => {
  loadingIcon.style.display = "flex"; // 👈 HIỆN LOADING
  loadVideo(true);

};
  
  
function updateEpisodeActiveUI(){
  const items = document.querySelectorAll("#episodeDropdown .adropdown-menu div");

  items.forEach(item=>{
    item.classList.remove("active");

    if(item.dataset.value === episode.value){
      item.classList.add("active");
    }
  });
}

episode.onchange = ()=>{
  loadingIcon.style.display = "flex";

  updateQualityOptions(episode.value);
  loadVideo(true);

  updateEpisodeActiveUI();   // 👈 THÊM DÒNG NÀY
};



/* ===== CLICK / DOUBLE CLICK ===== */

let clickTimer = null;

centerPlay.addEventListener("click", (e)=>{
  e.stopPropagation();

if(!src.src){
  loadingIcon.style.display = "flex";
  loadVideo(true);
  return;
}


  if(video.paused){
    video.play();
  } else {
    video.pause();
  }
});

video.addEventListener("click", (e) => {

  if (e.target.closest(".controls")) return;
  if (e.target.closest("#centerPlay")) return;
  if (e.target.closest(".settings")) return;

  if (clickTimer) clearTimeout(clickTimer);

  clickTimer = setTimeout(() => {
    video.paused ? video.play() : video.pause();
    clickTimer = null;
  }, 200);
});

video.addEventListener("dblclick", (e)=>{
  if(clickTimer){
    clearTimeout(clickTimer);
    clickTimer = null;
  }

  const rect = video.getBoundingClientRect();
  const x = e.clientX - rect.left;

  if(x < rect.width / 2){
    seekSafe(-10);
    showSeek("left", -10);
  }else{
    seekSafe(10);
    showSeek("right", 10);
  }
});



/* ===== PREVIEW (PC ONLY) ===== */

let raf = false;
  

function updatePreview(clientX){
  if (!video.duration || raf) return;

  raf = true;

  requestAnimationFrame(() => {
    const r = progress.getBoundingClientRect();
    let p = (clientX - r.left) / r.width;
    p = Math.max(0, Math.min(1, p));
    const time = p * video.duration;

    // 👇 DÁN TẠI ĐÂY
    const diff = Math.abs(vHidden.currentTime - time);

    if (isDragging && diff < 0.5) {
      raf = false;
      return;
    }

 if (diff > 1) {   // 👈 đổi 0.6 thành 1
  pendingDraw = true;
  vHidden.currentTime = time;
}

    const wrapRect = playerWrap.getBoundingClientRect();
    preview.style.left = (clientX - r.left - 80) + "px";
    preview.style.top = (r.top - wrapRect.top - 115) + "px";

    preview.style.display = "block";
    ptime.textContent = fmt(time);

    raf = false;
  });
}


if (!isMobile) {

  // 👉 HOVER
  progress.addEventListener("mousemove", e => {
    if (isDragging) return;
    updatePreview(e.clientX);
  });

  // 👉 RỜI THANH
progress.addEventListener("mouseleave", () => {
  preview.style.display = "none";
  pendingDraw = false;

  // 👇 ẨN TIME BUBBLE
  timeBubble.classList.remove("show");
  timeBubble.textContent = "";
});


} else {
  // MOBILE → tắt preview
  preview.style.display = "none";
  pendingDraw = false;
  vHidden.currentTime = 0;
}


/* ===== KEYBOARD ===== */

document.addEventListener("keydown", e=>{
  if(e.code === "Space"){
    e.preventDefault();
    video.paused ? video.play() : video.pause();
  }

if(e.code === "ArrowRight"){
  seekSafe(5);
  showSeek("right", 5);
}

if(e.code === "ArrowLeft"){
  seekSafe(-5);
  showSeek("left", -5);
}

});

/* ===== VOLUME ===== */

const savedVol = localStorage.getItem(VOL_KEY);
if(savedVol !== null){
video.volume = parseFloat(savedVol);
vol.value = parseFloat(savedVol);
}

let lastVolume = 0.5;

vol.oninput = ()=>{
  const v = parseFloat(vol.value);

  video.volume = v;
  video.muted = (v === 0);

  localStorage.setItem(VOL_KEY, v);

  if(v > 0){
    lastVolume = v;
  }
};
muteBtn.onclick = ()=>{
  if(video.muted || video.volume === 0){
    video.muted = false;
    video.volume = lastVolume;
    vol.value = lastVolume;
  }else{
    lastVolume = video.volume;
    video.muted = true;
  }
};

video.addEventListener("volumechange", updateMuteIcon);
updateMuteIcon();
  
const downloadBtn = playerWrap.querySelector("#downloadBtn");

function downloadCurrentVideo(){
let url = EPISODES[episode.value][quality.value];

  // force download Dropbox
  url = url.replace("dl=0","dl=1");

  const a = document.createElement("a");
  a.href = url;
  a.download = `video_${quality.value}p.mp4`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
}


function getFileName(){
  const q = quality.value;
  return `video_${q}p.mp4`;
}

downloadBtn.onclick = downloadCurrentVideo;
video.addEventListener("loadstart",()=>{
  downloadBtn.disabled = true;
  downloadBtn.style.opacity = "0.5";

  timeEl.textContent = "0:00 / 0:00";
  played.style.width = "0%";
    settings.classList.add("loading"); // ✅ THÊM

});


video.addEventListener("canplay",()=>{
  downloadBtn.disabled = false;
  downloadBtn.style.opacity = "1";
    loadingIcon.style.display = "none"; // 👈 TẮT LOADING
  settings.classList.remove("loading"); // ⛔ dừng xoay


});

const seekOverlay = document.getElementById("seekOverlay");
let seekTimer;

function showSeek(dir, sec){
  seekOverlay.classList.remove("left","right");

  if(dir === "left"){
    seekOverlay.classList.add("left");
  }else{
    seekOverlay.classList.add("right");
  }

  seekOverlay.innerHTML = `
    <div class="circle">
      ${dir === "left" ? arrowSVG("left") : ""}
      <span>${sec > 0 ? sec+"s+" : Math.abs(sec)+"s-"}</span>
      ${dir === "right" ? arrowSVG("right") : ""}
    </div>
  `;

  seekOverlay.style.opacity = 1;

  clearTimeout(seekTimer);
  seekTimer = setTimeout(()=>{
    seekOverlay.style.opacity = 0;
  }, 600);
}


const fsBtn = playerWrap.querySelector("#fsBtn");
const fsIcon = playerWrap.querySelector("#fsIcon");
  const fsDefaultSVG = fsIcon.innerHTML;


let isRotateLocked = false; // ← STATE XOAY (false = dọc, true = ngang)
const exitFullscreenIcon = `
<svg fill="#ffffff" height="24px" width="24px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M469.3,469.3H42.7V42.7H256L298.7,0h-256C19.1,0,0,19.1,0,42.7v426.7C0,492.9,19.1,512,42.7,512h426.7 c23.6,0,42.7-19.1,42.7-42.7V320l-42.7,42.7V469.3z M85.3,426.7C149.1,255.7,234.7,256,362.7,256v85.3L512,192L362.7,42.7V128 C85.3,128,85.1,341.1,85.3,426.7z"></path> </g></svg>
`;


const rotateSVG = `
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 243.988 243.988"
     width="24" height="24" fill="#ffffff">
  <g>
    <path d="M35.849,132.626c3.018,0,5.616-2.272,5.956-5.341c0.364-3.293-2.01-6.259-5.304-6.624C22.533,119.116,12,107.332,12,93.254c0-13.662,9.989-25.029,23.046-27.198l-8.751,8.751c-2.344,2.343-2.344,6.142,0,8.484c1.171,1.172,2.707,1.758,4.242,1.758c1.535,0,3.071-0.586,4.242-1.758l17.979-17.979c2.344-2.343,2.344-6.142,0.001-8.484L34.308,38.373c-2.344-2.343-6.144-2.344-8.485-0.001c-2.344,2.344-2.344,6.143-0.001,8.485l7.346,7.346C14.383,57.278,0,73.614,0,93.254c0,20.204,15.124,37.114,35.181,39.335C35.405,132.614,35.628,132.626,35.849,132.626z"/>
    <path d="M243.067,139.947c-3.872-23.728-15.358-30.204-22.151-31.937c-3.269-0.833-5.985-3.018-7.652-5.95c-5.37-9.448-13.27-11.682-18.871-11.917c-3.996-0.169-7.598-2.422-9.684-5.833c-5.386-8.807-15.197-14.507-25.77-14.507c-2.087,0-4.203,0.223-6.321,0.687c-0.82,0.18-1.646,0.267-2.465,0.267c-3.525,0-6.929-1.612-9.248-4.384c-4.832-5.773-13.367-15.138-26.534-26.881c-12.929-11.532-23.293-17.959-25.396-19.221c-0.272-0.164-0.538-0.344-0.797-0.529c-3.572-2.555-7.359-3.646-10.974-3.646C63.35,16.094,52.027,32.124,64.968,43.114c0.192,0.162,0.389,0.316,0.589,0.468c2.623,1.983,22.328,16.939,35.987,28.891c14.659,12.826,41.988,46.418,24.873,73.584c-5.341,8.476-14.138,11.134-22.588,11.134c-6.851,0-13.474-1.747-17.842-3.557c-6.075-2.787,0,0-19.534-8.963c-2.639-1.211-5.199-1.753-7.594-1.753c-14.82-0.003-23.411,20.707-6.616,31.517c19.509,12.556,38.748,17.595,48.367,31.795c6.456,9.53,17.75,22.698,26.152,30.24c2.236,2.007,5.128,3.424,8.133,3.424h68.725c5.726,0,10.675-4.301,11.879-9.899l2.554-12.028c0.316-1.47,0.907-2.942,1.73-4.2C240.642,181.847,246.508,161.029,243.067,139.947z"/>
  </g>
</svg>
`;



function lockLandscape(){
  screen.orientation?.lock("landscape").catch(()=>{});
}


function unlockOrientation(){
  if (screen.orientation && screen.orientation.unlock) {
    screen.orientation.unlock();
  }
}


function exitFullscreen(){
  if(document.exitFullscreen){
    document.exitFullscreen();
  }
}
  

fsBtn.onclick = () => {

  // 1️⃣ CHƯA fullscreen → vào fullscreen
if (!document.fullscreenElement) {
  playerWrap.requestFullscreen();
  return;
}

  // 2️⃣ fullscreen + dọc → xoay ngang
if (!isRotateLocked) {
  lockLandscape();
  isRotateLocked = true;

  fsIcon.innerHTML = exitFullscreenIcon;
  fsBtn.title = "Thoát toàn màn hình";

  // 👇 XOAY NGANG → BẮT ĐẦU AUTO-HIDE
  clearTimeout(hideTimer);
  hideTimer = setTimeout(() => {
    if (isDragging) return;
    playerWrap.classList.remove("show-controls");
    interacting = false;
  }, 2000);

  return;
}


  // 3️⃣ fullscreen + ngang → thoát
  exitFullscreen();
};


const total = Object.keys(EPISODES).length;
const digits = total >= 100 ? 3 : 2;

Object.keys(EPISODES).forEach((epKey, i)=>{
  const opt = document.createElement("option");
  opt.value = epKey;

  const number = String(i + 1).padStart(digits, "0");

  // 👇 Nếu là tập cuối → thêm END
  if(i === TOTAL_EP - 1){
    opt.textContent = `# ${number} - END`;
  }else{
    opt.textContent = `# ${number}`;
  }

  episode.appendChild(opt);
});


episode.value = "ep001";
updateQualityOptions("ep001");
document.querySelector("#qualityDropdown .adropdown-selected").textContent =
  quality.options[quality.selectedIndex].textContent;
/* ===== INIT DROPDOWN ===== */

function setupDropdown(id, items, onSelect){
  const dropdown = document.getElementById(id);
  if(!dropdown) return;

  const selected = dropdown.querySelector(".adropdown-selected");
  const menu = dropdown.querySelector(".adropdown-menu");

  menu.innerHTML = "";

items.forEach((text, index)=>{
  const div = document.createElement("div");
  div.textContent = text;

  // 🔥 THÊM DÒNG NÀY
  if(id === "episodeDropdown"){
    div.dataset.value = episode.options[index].value;
  }

    div.onclick = (e)=>{
      e.stopPropagation();
      selected.textContent = text;
      dropdown.classList.remove("open");
      if(onSelect) onSelect(text);
    };

    menu.appendChild(div);
  });

selected.onclick = (e)=>{
  e.stopPropagation();

  document.querySelectorAll(".adropdown").forEach(d=>{
    if(d !== dropdown) d.classList.remove("open");
  });
const quality=playerWrap.querySelector("#quality");
  dropdown.classList.toggle("open");

  // 🔥 THÊM PHẦN NÀY

};

}


  
/* 🔥 BUILD EPISODE DROPDOWN */
setupDropdown(
  "episodeDropdown",
  Array.from(episode.options).map(o => o.textContent),
  (text)=>{
    const index = Array.from(episode.options)
      .find(o => o.textContent === text).value;

    episode.value = index;
    episode.dispatchEvent(new Event("change"));
  }
);
updateEpisodeActiveUI();  // 👈 THÊM DÒNG NÀY

  
/* 🔥 BUILD QUALITY DROPDOWN */


document.addEventListener("click", (e)=>{

  // Đóng dropdown nếu click ngoài
  if(!e.target.closest(".adropdown")){
    document.querySelectorAll(".adropdown")
      .forEach(d=>d.classList.remove("open"));
  }

  // Đóng settings nếu click ngoài
  if (!settings.contains(e.target)) {
    settings.classList.remove("open");
  }

});



  
// 🔹 Tự động ẩn chọn tập nếu chỉ có 1 tập
if (Object.keys(EPISODES).length === 1) {
  document.getElementById("episodeDropdown").style.display = "none";

  // ép episode về tập duy nhất
  episode.value = Object.keys(EPISODES)[0];
}

/* ===== MOBILE FULLSCREEN CONTROLS ===== */


function showMobileControls(){
  if(!isMobile) return;
  if(!document.fullscreenElement) return;

  interacting = true;
  isOverControls = false; // 👈 reset nhẹ
  playerWrap.classList.add("show-controls");
  clearTimeout(hideTimer);
}

if(isMobile){
  video.addEventListener("touchstart", ()=>{
    if(document.fullscreenElement){
      showMobileControls();
    }
  });
}


// vào / thoát fullscreen
document.addEventListener("fullscreenchange", () => {

  if (document.fullscreenElement) {
    // VÀO FULLSCREEN

    if (isRotateLocked) {
      fsIcon.innerHTML = exitFullscreenIcon;
      fsBtn.title = "Thoát toàn màn hình";
    } else {
      fsIcon.innerHTML = rotateSVG;
      fsBtn.title = "Xoay ngang";
    }

    if (isMobile) {
      showMobileControls();
    }

  } else {
    // THOÁT FULLSCREEN
    unlockOrientation();
    isRotateLocked = false;

    fsIcon.innerHTML = fsDefaultSVG;
    fsBtn.title = "Toàn màn hình";

    playerWrap.classList.remove("show-controls");
  }

});






// pause thì giữ controls
video.addEventListener("pause", ()=>{
  
  playIcon.innerHTML = `<polygon points="6,4 20,12 6,20"/>`;
  centerPlayIcon.innerHTML = `<polygon points="6,4 20,12 6,20"/>`;
  centerPlay.classList.remove("hide");

  if (isMobile && document.fullscreenElement && isRotateLocked) {
    clearTimeout(hideTimer);      // ⛔ chặn auto-hide
    interacting = true;           // ⛔ ép coi như đang tương tác
    isOverControls = true;
    playerWrap.classList.add("show-controls");
  }
});


function showControls(){
  playerWrap.classList.add("show-controls");
  clearTimeout(hideTimer);
}

function scheduleHide(){
  clearTimeout(hideTimer);
  hideTimer = setTimeout(()=>{
    if (interacting || isDragging || isOverControls) return; // 👈 CỐT LÕI
    playerWrap.classList.remove("show-controls");
  }, 2000);
}


playerWrap.addEventListener("mousemove", (e) => {
  if (!document.fullscreenElement) return;
  if (isDragging) return;

  const el = document.elementFromPoint(e.clientX, e.clientY);
  const overControls =
    el &&
    (controls.contains(el) || progress.contains(el));

  if (overControls) {
    interacting = true;
    isOverControls = true;
    clearTimeout(hideTimer);
    showControls();
    return; // ⛔ TUYỆT ĐỐI KHÔNG scheduleHide
  }

  interacting = false;
  isOverControls = false;
  showControls();
  scheduleHide();
});


const settingsBtn = document.getElementById("settingsBtn");

settingsBtn.onclick = (e)=>{
  e.stopPropagation();

  showControls();              // 🔥 thêm dòng này
  playerWrap.classList.add("show-controls"); // 🔥 thêm

  settings.classList.toggle("open");
};

const settingsMenu = settings.querySelector(".settings-menu");

settingsMenu.addEventListener("click", (e) => {
  e.stopPropagation();
});



video.addEventListener("dragstart", e => {
  e.preventDefault();
});


// click ngoài thì đóng hết
document.addEventListener("click", function(){
  document.querySelectorAll(".adropdown").forEach(drop => {
    drop.classList.remove("open");
  });
});


function updateQualityDropdownUI(){
  const menu = document.querySelector("#qualityDropdown .adropdown-menu");
  const selected = document.querySelector("#qualityDropdown .adropdown-selected");

  menu.innerHTML = "";

  [...quality.options].forEach(opt=>{
    const item = document.createElement("div");
    item.className = "adropdown-item";
    item.dataset.value = opt.value;

    let label = "";

    if(opt.value == "360") label = "SD";
    if(opt.value == "480") label = "FSD";
    if(opt.value == "720") label = "HD";
    if(opt.value == "1080") label = "FHD";
    if(opt.value == "1440") label = "QHD";

    item.innerHTML =
      opt.textContent +
      (label ? `<span class="q-label ${label}">${label}</span>` : "");

item.onclick = (e)=>{
  e.stopPropagation();        // 🔥 thêm dòng này
      quality.value = opt.value;
      selected.textContent = opt.textContent;
      document.getElementById("qualityDropdown").classList.remove("open");
      quality.dispatchEvent(new Event("change"));
    };

    menu.appendChild(item);
  });

  selected.textContent =
    quality.options[quality.selectedIndex]?.textContent || "";
}
function lockLandscape(){

  const wasPlaying = !video.paused;

  if (wasPlaying) video.pause();

  screen.orientation?.lock("landscape").then(()=>{

    requestAnimationFrame(()=>{
      if (wasPlaying) {
        video.play().catch(()=>{});
      }
    });

  }).catch(()=>{});

}
</script>


🎯 Kết luận


Trên đây là toàn bộ phần giới thiệu và code của trình phát video HTML5 mà mình đã tự xây dựng. Mục tiêu ban đầu chỉ là làm một player “đủ dùng”, nhưng càng làm càng cuốn, và cuối cùng nó trở thành một hệ thống player hoàn chỉnh với trải nghiệm không thua kém các nền tảng lớn.

Điều mình thấy đáng giá nhất không phải là số lượng tính năng, mà là cách mọi thứ hoạt động cùng nhau một cách mượt mà — từ UI, animation cho tới logic xử lý phía sau. Tất cả đều được viết bằng HTML, CSS và JavaScript thuần, không phụ thuộc thư viện ngoài, nên rất dễ tùy biến và tích hợp vào bất kỳ website nào (đặc biệt là Blogspot).

Nếu bạn đang muốn nâng cấp trải nghiệm video trên blog/web của mình, hoặc đơn giản là muốn học cách xây dựng một player chuyên nghiệp từ đầu, thì hy vọng đoạn code này sẽ giúp ích cho bạn.


Nếu thấy hay, bạn có thể:

👉 Tùy biến thêm giao diện theo style riêng

👉 Thêm subtitle, tốc độ phát, hoặc API riêng

👉 Hoặc dùng làm base cho một web phim hoàn chỉnh 🚀



Hashtag: Code trình phát video HTML5, thế hệ mới 2026

Thứ Ba

Lemon And Strawberry Emoticons | Biểu tượng cảm xúc

Bạn thắc mắc mấy cái Emoticons này để làm cái quần què gì ư? À thì bạn có thể làm nhiều thứ ví dụ như... lụm link ảnh chèn vào phần bình luận trên blog này cho đẹp nhé!

Phần tiếp theo là bộ Lemon And Strawberry Emoticons 😗



Hashtag: Lemon And Strawberry Emoticons | Biểu tượng cảm xúc

Thứ Tư

Ảnh động Fuyuko Mayuzumi cho Quý Bửu tự kỷ

Méo có gì giới thiệu, xem title là biết. 👀
Tên bộ ảnh: Ảnh động Fuyuko Mayuzumi cho Quý Bửu tự kỷ
Ngày phát hành: 28/08/2025
Image Count: 1/1 - Full
Artist: おひや | AI-generated



Hashtag: Ảnh động Fuyuko Mayuzumi cho Quý Bửu tự kỷ

Chủ Nhật

Red Fox Emoticons | Biểu tượng cảm xúc

Bạn thắc mắc mấy cái Emoticons này để làm cái quần què gì ư? À thì bạn có thể làm nhiều thứ ví dụ như... lụm link ảnh chèn vào phần bình luận trên blog này cho đẹp nhé!

Phần tiếp theo là bộ Red Fox Emoticons 😗



Hashtag: Red Fox Emoticons | Biểu tượng cảm xúc

Thứ Sáu

[AMV] secret base ~Kimi ga Kureta Mono~ (10 years after ver.) - Ai Kayano, Haruka Tomatsu & Saori Hayami (Vietsub)

Đã có bao giờ bạn từng nghĩ sẽ viết một bức thư gửi đến chính bạn của 10 năm sau? 10 năm sau bạn thế nào?

Với ca khúc: secret base ~Kimi ga Kureta Mono~ (10 years after ver.) (secret base 〜君がくれたもの〜(10 years after Ver.)
Căn cứ bí mật ~Điều bạn đã trao cho tôi~ (Phiên bản sau 10 năm) chắc hẳn đã ngốn không biết bao nhiêu nước mắt người nghe và xem.

(Tạm dịch: Căn cứ bí mật - Gửi đến tôi của 10 năm sau)
Điều đặc biệt hơn thế nữa là ca sĩ trình bay ca khúc này không ai khác chính là Meiko Honma (Ai Kayano), Naruko Anjou (Haruka Tomatsu), Chiriko Tsurumi (Saori Hayami) những diễn viên lồng tiếng của ba nhân vật nữ chính trong anime này.



Hashtag: [AMV] secret base ~Kimi ga Kureta Mono~ (10 years after ver.) - Ai Kayano, Haruka Tomatsu & Saori Hayami (Vietsub)

Template Blogger: Hiền Tài X Version 3.0 Premium Free - Mẫu giao diện đẹp thích hợp cho blog 18+

Lâu không up gì thôi thì nay làm bài chia sẻ cái theme, đây mà mẫu giao diện mình đang dùng cho blog hiền tài.

Template được phát triển dựa trên nền tảng Magma 2.0 của Templateify, bản gốc có giá bán là $10, ở đây bạn sẽ có phiên bản 3.0 cải tiến Premium hoàn toàn miễn phí.

Template Blogger: Hiền Tài X Version 3.0 Premium Free



Hashtag: Template Blogger: Hiền Tài X Version 3.0 Premium Free - Mẫu giao diện đẹp thích hợp cho blog 18+
Trang sau ⇨ Trang chủ