Do you know how to force video loading in iOS Safari ?
Introduction
最近開發一個 image / video uploader,裡面有個基本訴求 — 當使用者選取檔案的當下要即時繪製對應的縮圖出來。這需求其實不難,透過現有支援的 Web API 便可以輕鬆完成。
其核心思想與步驟如下:
- 透過 Web API 先將使用者選取的 blob 格式轉換成 dataURL。
- 利用 image 以及 video tag 讀取上一步驟取得的 dataURL。
- 掛上對應的監聽,在檔案完成 load 的當下透過 canvas 進行繪製。
如此一來,便可以隨心所欲的繪製出 crop / resize 的縮圖了。照理說…核心思想有了,那麼實作上應該沒什麼太大的問題才是,但是筆者卻在 iOS Safari 上踢到了鐵板~
Issue
在 iOS Safari 上筆者遇到的直接問題是 video 類型的檔案其縮圖的繪製時有時無。筆者自己在操作上若是一步一步走則基本上縮圖就會做不出來,然而若是動作快一些則有機會成功做出縮圖。
基本上這種不安定最是難除錯,總不可能每次都叫 user 各個動作銜接上要快一點才行。也因此困擾了筆者好一段時間。
Solution
由於要繪製 video 的縮圖必須要監聽 loadeddata event。待 event 觸發之後才能透過 canvas 的 drawImage() 進行繪製,而 iOS Safari 並不是可以很穩定的觸發該 event。
會造成這現象主要就是 iOS 的保護機制,除非有 user interaction 發生,不然 iOS Safari 不會輕易的去 load data,自然就無法觸發 loadeddata event 了。也就是說只要想出一個方法可以強迫 iOS Safari 進行 video loading 的行為便能迎刃而解了。
以下為對應的相關思路以及 code:
Apply autoplay to trigger video loading
筆者首先想到的便是 attribute — autoplay,這是唯一一個不需要 user interaction 就可以 trigger video play 的方法。而要讓 autoplay 可以作用的先決條件便是搭配 mute,如此一來便能有效的驅動 video loading。
<script type="module">
const video = document.createElement('video');
/* force loading */
video.autoplay = true;
video.muted = true;
video.src = 'your-video-path';
</script>Avoid video fullscreen play
由於 iOS Safari 如果沒有特別設置的話,那麼 video play 的 default 行為便是會全螢幕播放,這對我們會造成困擾,畢竟此為非預期行為。要避免 fullscreen play 的方式便是透過 attribute — playsinline 的設置,如此便可以讓它原地播放。
<script type="module">
const video = document.createElement('video');
/* force loading */
video.autoplay = true;
video.muted = true;
/* avoid fullscreen play */
video.playsInline = true;
video.src = 'your-video-path';
</script>Tell browser about video loading strategy
我們可以透過 attribute — preload 來告知瀏覽器是否該進行 preload,設定 auto 即表示該檔案可以被完整 download 即使 user 從頭到尾都沒有看過它。
<script type="module">
const video = document.createElement('video');
/* force loading */
video.autoplay = true;
video.muted = true;
/* avoid fullscreen play */
video.playsInline = true;
/* tell browser about video loading strategy */
video.preload = 'auto';
video.src = 'your-video-path';
</script>Add stylesheet to hide <video />
既然要「藏」,就要藏得徹底一點,所以可以適時的設置一些 style 避免 video 露餡。
<script type="module">
const video = document.createElement('video');
/* force loading */
video.autoplay = true;
video.muted = true;
/* avoid fullscreen play */
video.playsInline = true;
/* tell browser about video loading strategy */
video.preload = 'auto';
/* Add stylesheet to hide <video /> */
video.style.visibility = 'hidden';
video.style.pointerEvents = 'none';
video.src = 'your-video-path';
</script>
<video>: The Video Embed element > preload筆者在這裡用的分別是 visibility 以及 pointer-events 兩個屬性。透過這兩個屬性結合,讓 user 不僅看不到也摸不到。之所以不用 display 是因為這屬性很特別,在不同瀏覽器的詮釋可能會不同,若貿然設置 display: none,可能會 trigger 瀏覽器不要 loading,如此一來就違背本篇的初衷了。
Call <video /> method — load()
完成了上面這些設置後,基本上應該就可以正常驅動 video loading 了,如果擔心的話,還是可以多加上最後一道保險 — load()。直接 call video tag 原生提供的 method — load 來進行 trigger。
<script type="module">
const video = document.createElement('video');
/* force loading */
video.autoplay = true;
video.muted = true;
/* avoid fullscreen play */
video.playsInline = true;
/* tell browser about video loading strategy */
video.preload = 'auto';
/* Add stylesheet to hide <video /> */
video.style.visibility = 'hidden';
video.style.pointerEvents = 'none';
video.src = 'your-video-path';
video?.load?.();
</script>Do not forget add 「loadeddata」event listener
最後亦是最重要的一步驟,就是不要忘記加上 loadeddata 這個 event listener。如此一來便可以在 video 完成基本的 loading 後立即觸發該 event 了。
<script type="module">
const video = document.createElement('video');
/* event listener */
video.onloadeddata = () => {
// do something you need
};
/* force loading */
video.autoplay = true;
video.muted = true;
/* avoid fullscreen play */
video.playsInline = true;
/* tell browser about video loading strategy */
video.preload = 'auto';
/* Add stylesheet to hide <video /> */
video.style.visibility = 'hidden';
video.style.pointerEvents = 'none';
video.src = 'your-video-path';
video?.load?.();
</script>Conclusion
以上便是困擾我許久的除錯過程,簡單幾個小步驟便能有效地讓 iOS Safari 上的 video 進行 loading。
以上便是今天的分享,感謝大家的閱讀,希望所提供的分享對大家均有所助益~