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ẩnwebkitbeginfullscreentrê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.
mutedset quá trễ — property phảitruetrướcplay().- 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ặcpreload="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
playsinlinedù cócontrols—controlsthê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ử:
- Bỏ
playsinline, Reload, bấm play() → trên iPhone nó bung fullscreen. - Bỏ
muted, Reload → promise autoplay bị từ chối vớiNotAllowedError. - 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/catchvớ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: