Kiến thức cơ bản về tạo kiểu trình phát video

Trong bài viết Trình phát video đa trình duyệt trước đó, chúng ta đã mô tả cách xây dựng một trình phát video HTML đa trình duyệt bằng Media và Fullscreen API. Bài viết tiếp theo này xem xét cách tạo kiểu cho trình phát tùy biến đó, bao gồm làm cho nó đáp ứng.

Đánh dấu HTML

Có một vài thay đổi đã được thực hiện đối với phần đánh dấu HTML ở bài viết trước. Các điều khiển video tùy biến và phần tử <progress> hiện được chứa trong các phần tử <div>, thay vì nằm bên trong các mục danh sách không có thứ tự.

Phần đánh dấu cho các điều khiển tùy biến hiện trông như sau:

html
<div id="video-controls" class="controls" data-state="hidden">
  <button id="play-pause" type="button" data-state="play">Play/Pause</button>
  <button id="stop" type="button" data-state="stop">Stop</button>
  <div class="progress">
    <progress id="progress" value="0">
      <span id="progress-bar"></span>
    </progress>
  </div>
  <button id="mute" type="button" data-state="mute">Mute/Unmute</button>
  <button id="vol-inc" type="button" data-state="vol-up">Vol+</button>
  <button id="vol-dec" type="button" data-state="vol-down">Vol-</button>
  <button id="fs" type="button" data-state="go-fullscreen">Fullscreen</button>
</div>

Thuộc tính data-state được dùng ở nhiều vị trí cho mục đích tạo kiểu và được thiết lập bằng JavaScript. Các cách triển khai cụ thể sẽ được nhắc đến ở những chỗ phù hợp bên dưới.

Tạo kiểu

Kiểu dáng trình phát video thu được ở đây khá cơ bản - điều này có chủ ý, vì mục tiêu là cho thấy một trình phát video như vậy có thể được tạo kiểu và làm cho đáp ứng như thế nào.

Note: Trong một số trường hợp, một ít CSS cơ bản bị lược bớt khỏi các ví dụ mã ở đây vì việc sử dụng chúng hoặc là hiển nhiên, hoặc không liên quan trực tiếp đến việc tạo kiểu cho trình phát video.

Tạo kiểu cơ bản

Bản thân vùng chứa các điều khiển video cần một chút tạo kiểu để được thiết lập đúng cách:

css
.controls {
  display: flex;
  align-items: center;
  overflow: hidden;
  width: 100%;
  height: 2rem;
  position: relative;
}

Vị trí được đặt thành relative, điều này cần thiết cho khả năng đáp ứng của nó (sẽ nói thêm ở phần sau).

Như đã đề cập trước đó, thuộc tính data-state được dùng để cho biết các điều khiển video có hiển thị hay không và nó cần các khai báo CSS tương ứng:

css
.controls[data-state="hidden"] {
  display: none;
}

Các nút

Công việc tạo kiểu lớn đầu tiên cần xử lý là làm cho các nút điều khiển video thực sự trông và hoạt động như nút bấm thật.

Mỗi nút có một số kiểu cơ bản:

css
.controls button {
  width: 2rem;
  height: 2rem;
  text-align: center;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  border: none;
  cursor: pointer;
  color: transparent;
  background-color: transparent;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}

Mỗi nút được đặt chiều rộng và chiều cao là 2rem. Theo mặc định, tất cả phần tử <button> đều có viền, nên viền này được bỏ đi. Vì sẽ dùng ảnh nền để hiển thị biểu tượng phù hợp, màu nền của nút được đặt trong suốt, không lặp, và phần tử phải chứa vừa toàn bộ ảnh. Ngoài ra, có một ít văn bản nhãn không nên hiển thị trên màn hình, nên màu chữ được đặt thành trong suốt.

Sau đó trạng thái :hover:focus được đặt cho từng nút để thay đổi độ mờ của nút:

css
.controls button:hover,
.controls button:focus {
  opacity: 0.5;
}

Để có được ảnh nút phù hợp, chúng ta đã tải xuống một bộ biểu tượng điều khiển chung miễn phí từ web. Mỗi ảnh sau đó được chuyển thành chuỗi mã hóa base64 (dùng một trình mã hóa ảnh base64 trực tuyến), vì các ảnh này khá nhỏ nên các chuỗi mã hóa thu được cũng khá ngắn.

Vì một số nút có hai chức năng, ví dụ như phát/tạm dừng và tắt tiếng/bật tiếng, các nút này có những trạng thái khác nhau cần được tạo kiểu. Như đã nói trước đó, một biến data-state được dùng để cho biết các nút như vậy hiện đang ở trạng thái nào.

Ví dụ, nút phát/tạm dừng có các định nghĩa ảnh nền sau (các chuỗi base64 đầy đủ được lược bớt cho ngắn gọn):

css
.controls button[data-state="play"] {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAA…");
}

.controls button[data-state="pause"] {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAA…");
}

Khi data-state của nút thay đổi, ảnh phù hợp cũng sẽ thay đổi theo. Tất cả các nút còn lại được xử lý theo cách tương tự.

Thanh tiến trình

Vùng chứa <div> cho phần tử <progress> được bật flex-grow, để nó giãn ra lấp phần không gian còn lại trong vùng điều khiển. Nó cũng hiển thị con trỏ dạng bàn tay để cho biết phần tử này có thể tương tác.

css
.controls .progress {
  flex-grow: 1;
  cursor: pointer;
  height: 80%;
}

Phần tử <progress> được thiết lập kiểu cơ bản như sau:

css
.controls progress {
  display: block;
  width: 100%;
  height: 100%;
  border: none;
  color: #0095dd;
  border-radius: 2px;
  margin: 0 auto;
}

Giống như các phần tử <button>, <progress> cũng có viền mặc định, và viền này được bỏ đi ở đây. Nó cũng được bo góc nhẹ vì mục đích thẩm mỹ.

Có một số thuộc tính riêng theo trình duyệt cần được đặt để đảm bảo Firefox và Chrome dùng đúng màu cho thanh tiến trình:

css
.controls progress::-moz-progress-bar {
  background-color: #0095dd;
}

.controls progress::-webkit-progress-value {
  background-color: #0095dd;
}

Mặc dù cùng một thuộc tính được gán cùng một giá trị, những quy tắc này vẫn cần được định nghĩa riêng biệt, nếu không toàn bộ khai báo có thể trở thành hợp lệ khi một selector nào đó không được nhận diện.

Toàn màn hình

Giờ hãy tạo kiểu cho điều khiển ở chế độ toàn màn hình. Vì phần tử <figure> là phần được đưa vào toàn màn hình, chúng ta có thể nhắm tới nó bằng giả lớp :fullscreen. Chúng ta sẽ làm vài việc:

  • Làm cho figure chiếm toàn bộ màn hình với height: 100%
  • Giữ thanh điều khiển ở đáy trong khi video vẫn được căn giữa bằng flexbox
  • Làm cho vùng chứa trong suốt để hiển thị màu nền gốc của trình duyệt
  • Ẩn figcaption
  • Khôi phục màu nền cho hàng điều khiển để đảm bảo các nút màu đen vẫn nhìn thấy được khi nền gốc là màu đen.
css
figure:fullscreen {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  max-width: 100%;
  height: 100%;
  background-color: transparent;
}
figure:fullscreen video {
  margin-top: auto;
  margin-bottom: auto;
}
figure:fullscreen figcaption {
  display: none;
}
figure:fullscreen .controls {
  background-color: #666666;
}

Tạo kiểu đáp ứng

Bây giờ khi trình phát đã có giao diện và cảm nhận cơ bản, cần thực hiện thêm một số thay đổi tạo kiểu khác - liên quan đến media query - để làm cho nó đáp ứng.

Chúng ta muốn tùy chỉnh bố cục điều khiển khi xem trên màn hình nhỏ hơn (680px/42.5em), nên một điểm ngắt được định nghĩa ở đây. Chúng ta điều chỉnh kích thước và vị trí của các nút và thanh tiến trình để chúng sắp xếp khác đi:

css
@media screen and (width <= 42.5em) {
  .controls {
    height: auto;
  }

  .controls button {
    width: calc(100% / 6);
    margin-top: 2.5rem;
  }

  .controls .progress {
    position: absolute;
    top: 0;
    width: 100%;
    margin-top: 0;
    height: 2rem;
  }

  .controls .progress progress {
    width: 98%;
  }

  figcaption {
    text-align: center;
  }
}

Vùng chứa .progress giờ được chuyển lên đầu bộ điều khiển bằng position:absolute, vì vậy chính nó và toàn bộ các nút đều phải rộng hơn. Ngoài ra, các nút cần được đẩy xuống dưới vùng chứa tiến trình để chúng hiển thị được.

JavaScript

Về mặt tạo kiểu ngay lập tức thì gần như chỉ có vậy; nhiệm vụ tiếp theo là thực hiện một số thay đổi JavaScript để đảm bảo mọi thứ hoạt động như mong đợi, chủ yếu là tái cấu trúc logic của các nút.

Phát/Tạm dừng và tắt tiếng

Bây giờ các nút đã thực sự trông giống nút bấm và có biểu tượng thể hiện chức năng của chúng, cần thực hiện một số thay đổi để các nút có "chức năng kép" (như nút phát/tạm dừng) ở đúng "trạng thái" và hiển thị đúng ảnh. Để hỗ trợ việc này, một hàm mới được định nghĩa là changeButtonState(), nhận một biến kiểu cho biết chức năng của nút:

js
function changeButtonState(type) {
  if (type === "play-pause") {
    // Nút Phát/Tạm dừng
    if (video.paused || video.ended) {
      playPause.setAttribute("data-state", "play");
    } else {
      playPause.setAttribute("data-state", "pause");
    }
  } else if (type === "mute") {
    // Nút tắt tiếng
    mute.setAttribute("data-state", video.muted ? "unmute" : "mute");
  }
}

Hàm này sau đó được gọi bởi các bộ xử lý sự kiện liên quan:

js
video.addEventListener("play", () => {
  changeButtonState("play-pause");
});

video.addEventListener("pause", () => {
  changeButtonState("play-pause");
});

stop.addEventListener("click", (e) => {
  video.pause();
  video.currentTime = 0;
  progress.value = 0;

  // Cập nhật 'data-state' của nút phát/tạm dừng để ảnh nút đúng
  // có thể được đặt thông qua CSS
  changeButtonState("play-pause");
});

mute.addEventListener("click", (e) => {
  video.muted = !video.muted;
  changeButtonState("mute");
});

Bạn có thể nhận thấy có các bộ xử lý mới khi sự kiện playpause được xử lý trên video. Có lý do cho điều này! Dù bộ điều khiển mặc định của trình duyệt đã bị tắt, nhiều trình duyệt vẫn cho phép truy cập chúng bằng cách nhấp chuột phải trên thẻ video HTML. Điều đó có nghĩa là người dùng có thể phát/tạm dừng video từ những điều khiển này, khiến các nút của bộ điều khiển tùy biến bị lệch trạng thái. Nếu người dùng dùng điều khiển mặc định, các sự kiện Media API đã định nghĩa - như playpause - sẽ được kích hoạt, và ta có thể tận dụng điều đó để đảm bảo các nút điều khiển tùy biến luôn đồng bộ. Cú nhấp của chúng ta cũng kích hoạt các sự kiện playpause khi gọi các phương thức play()pause(), nên không cần thay đổi gì ở đây:

js
playPause.addEventListener("click", (e) => {
  if (video.paused || video.ended) {
    video.play();
  } else {
    video.pause();
  }
});

Âm lượng

Hàm alterVolume(), được gọi khi các nút âm lượng của trình phát được nhấn, cũng thay đổi - giờ nó gọi thêm một hàm mới là checkVolume():

js
function checkVolume(dir) {
  if (dir) {
    const currentVolume = Math.floor(video.volume * 10) / 10;
    if (dir === "+" && currentVolume < 1) {
      video.volume += 0.1;
    } else if (dir === "-" && currentVolume > 0) {
      video.volume -= 0.1;
    }

    // Nếu âm lượng đã bị tắt, cũng đặt nó ở trạng thái muted
    // Lưu ý: chỉ làm được điều này với bộ điều khiển tùy biến, vì khi sự kiện 'volumechange' được kích hoạt,
    // không có cách nào biết nó là do thay đổi âm lượng hay thay đổi trạng thái tắt tiếng
    video.muted = currentVolume <= 0;
  }
  changeButtonState("mute");
}

function alterVolume(dir) {
  checkVolume(dir);
}
volInc.addEventListener("click", (e) => {
  alterVolume("+");
});
volDec.addEventListener("click", (e) => {
  alterVolume("-");
});

Hàm checkVolume() mới này làm cùng việc như alterVolume(), nhưng nó còn đặt trạng thái của nút tắt tiếng tùy theo thiết lập âm lượng hiện tại của video. checkVolume() cũng được gọi khi sự kiện volumechange được kích hoạt:

js
video.addEventListener("volumechange", () => {
  checkVolume();
});

Tiến trình và toàn màn hình

Phần triển khai thanh tiến trìnhtoàn màn hình không thay đổi.

Kết quả

Warning: Video ví dụ có thể to!