jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Why Video Won't Play on iOS — playsinline, Autoplay & the play() Promise

The real reasons a <video> goes fullscreen, refuses to autoplay, or stays silent on iPhone — and the exact playsinline + muted + autoplay recipe (plus a tap-to-play fallback) that fixes it. With a live demo.

Bạn thả một <video autoplay> vào trang, nó chạy ngon trên laptop, bạn ship — rồi trên iPhone nó hoặc chiếm trọn màn hình, hoặc không chịu autoplay, hoặc phát mà không có tiếng. Không cái nào là bug: đó là iOS cố tình bảo vệ người dùng khỏi video bung fullscreen bất ngờ, tiếng ồn tự phát, và hao pin.

Bài này là cẩm nang về các luật đó và đúng những attribute để thỏa mãn chúng.

iOS có ba luật riêng biệt

Phần lớn rối rắm về “video iOS” đến từ việc gộp chúng làm một. Thực ra là ba:

 ┌─────────────────┬──────────────────────────────┬───────────────────────────┐
 │  Rule           │  Default on iPhone            │  How to satisfy it        │
 ├─────────────────┼──────────────────────────────┼───────────────────────────┤
 │  Inline play    │  forces NATIVE fullscreen     │  playsinline              │
 │  Autoplay       │  blocked                      │  muted + playsinline      │
 │  Sound on       │  needs a user gesture         │  user tap → unmute/play   │
 └─────────────────┴──────────────────────────────┴───────────────────────────┘

Giải theo thứ tự. Inline trước (để video nằm trong layout), rồi autoplay (để tự chạy), rồi tiếng (chỉ sau một cú chạm).


playsinline: chặn việc bung toàn màn hình

Mặc định, iPhone (không phải iPad) phát <video> trong trình phát toàn màn hình gốc ngay khi bắt đầu. Điều đó phá hỏng video nền, video sản phẩm dạng loop, và bất kỳ video nào cần nằm bên trong thiết kế của bạn.

Cách sửa là một attribute:

<video src="/clip.mp4" playsinline webkit-playsinline></video>
  • attribute HTML tiêu chuẩn.
  • phiên bản tiền tố cũ cho iOS đời cũ (iOS 9 trở xuống); giữ lại cho chắc cũng vô hại.

Trong JavaScript, property viết camelCase: video.playsInline = true.

Không có playsinline, bạn có thể phát hiện việc bung màn hình: iOS bắn một event phi tiêu chuẩn webkitbeginfullscreen trên element. Demo bên dưới ghi log nó để bạn thấy khoảnh khắc đó.


autoplay cần muted

Các trình duyệt hiện đại chặn autoplay có tiếng. iOS nghiêm nhất. Công thức autoplay đáng tin duy nhất là cả ba cùng lúc:

<video
  src="/clip.mp4"
  autoplay
  muted
  playsinline
  loop
></video>

Điểm tinh tế là muted: chính sách autoplay kiểm tra property muted, không chỉ là attribute. Nếu bạn tạo element bằng JS, hãy set property một cách tường minh trước khi gọi play:

const video = document.createElement('video');
video.src = '/clip.mp4';
video.playsInline = true;
video.muted = true;        // ✅ property — the policy reads this
video.setAttribute('muted', ''); // attribute too, for markup parity
video.autoplay = true;

Một cái bẫy hay gặp: chỉ set video.setAttribute('muted','') sau khi element đã tồn tại có thể để property vẫn false, và autoplay thất bại trong im lặng.


play() trả về một promise có thể bị từ chối

HTMLMediaElement.play() là bất đồng bộ và trả về một promise. Khi trình duyệt từ chối (autoplay chưa muted, chế độ tiết kiệm pin, không có gesture), promise đó bị reject với NotAllowedError — nó không throw đồng bộ. Nếu bạn bỏ qua, video chỉ nằm im và bạn nhận một unhandled rejection trong console.

Luôn xử lý nó, và dự phòng bằng một nút chạm-để-phát:

async function startVideo(video, playButton) {
  try {
    await video.play();
    playButton.hidden = true;          // autoplay worked
  } catch (err) {
    // NotAllowedError → the browser blocked it. Show a tap target;
    // a real user gesture is the one thing it will always accept.
    playButton.hidden = false;
    playButton.addEventListener('click', () => video.play(), { once: true });
  }
}

Quy tắc vàng: một cú gesture thật của người dùng (chạm/click) làm được mọi thứ — phát có tiếng, fullscreen, bỏ mute. Mọi thứ tự động đều bị hạn chế.


Những cái bẫy ngốn cả buổi chiều

  • Chế độ tiết kiệm pin — iOS chặn cả autoplay đã muted khi bật tiết kiệm pin. Không có cách ép; nút chạm-để-phát là lối thoát duy nhất.
  • muted set quá trễ — property phải true trước play().
  • thiết lập tiết kiệm dữ liệu / mạng di động — Safari có thể từ chối preload qua mạng di động; dùng preload="metadata" hoặc preload="none" rồi phát theo yêu cầu.
  • sai codec/định dạng — iOS cần H.264/HEVC trong MP4. File chỉ có VP9/WebM sẽ không giải mã được.
  • inline vẫn cần playsinline dù có controlscontrols thêm thanh tua nhưng không chặn việc bung fullscreen khi phát.

Thử ngay — demo trực tiếp

Bật/tắt từng attribute và xem thẻ <video> được sinh ra, trạng thái phát, và log sự kiện. Trên desktop mọi thứ phát inline; hành vi riêng của iOS (bung fullscreen, promise bị từ chối) nên xem trên iPhone thật.

Mở demo đầy đủ:

Những thứ nên thử:

  1. Bỏ playsinline, Reload, bấm play() → trên iPhone nó bung fullscreen.
  2. Bỏ muted, Reload → promise autoplay bị từ chối với NotAllowedError.
  3. Tự bấm nút play() (một cú chạm thật) khi có tiếng → thành công, vì gesture của người dùng được phép.

Công thức copy-paste

Một video nền muted, loop, phát inline ở mọi nơi và xuống cấp êm ái:

<video
  class="hero-video"
  src="/hero.mp4"
  autoplay
  muted
  loop
  playsinline
  webkit-playsinline
  preload="metadata"
  poster="/hero-poster.jpg"
></video>
<button class="hero-play" hidden>Tap to play</button>
const video = document.querySelector('.hero-video');
const playButton = document.querySelector('.hero-play');

// muted must be true as a PROPERTY for the autoplay policy.
video.muted = true;

video.play().catch(() => {
  // Blocked (e.g. Low Power Mode). Reveal a tap target — a gesture always works.
  playButton.hidden = false;
  playButton.addEventListener('click', () => {
    playButton.hidden = true;
    video.play();
  }, { once: true });
});

Checklist trước khi ship

  • trên mọi video inline
  • set như một property khi cần autoplay
  • cho video nền
  • bọc trong try/catch với dự phòng chạm-để-phát
  • MP4 với H.264/HEVC
  • để tôn trọng giới hạn dữ liệu
  • Đã test trên iPhone thật, kể cả khi bật tiết kiệm pin

iOS không khó tính cho vui: mỗi luật ứng với một lời than phiền nó từng nhận — fullscreen bất ngờ, tiếng ồn tự phát, cạn pin. Thỏa mãn các luật và video của bạn sẽ hành xử giống nhau ở mọi nơi.

Tham khảo: