วิธีเร่งการเล่นสื่อด้วยการโหลดทรัพยากรล่วงหน้าอย่างสม่ำเสมอ
การเริ่มเล่นที่เร็วขึ้นหมายความว่ามีผู้ชมวิดีโอหรือฟังเสียงของคุณมากขึ้น นั่นเป็นข้อเท็จจริงที่ทราบกันดี ในบทความนี้ เราจะกล่าวถึงเทคนิคที่คุณสามารถใช้เพื่อเร่งการเล่นเสียงและวิดีโอด้วยการโหลดทรัพยากรล่วงหน้าอย่างสม่ำเสมอ ทั้งนี้ขึ้นอยู่กับกรณีการใช้งาน
เราจะอธิบายวิธี 3 วิธีในการโหลดไฟล์สื่อล่วงหน้า โดยเริ่มจากข้อดีและข้อเสีย
เยี่ยมเลย | แต่... | |
---|---|---|
แอตทริบิวต์การโหลดวิดีโอล่วงหน้า | ใช้งานง่ายสำหรับไฟล์ที่ไม่ซ้ำกันซึ่งโฮสต์ในเว็บเซิร์ฟเวอร์ | เบราว์เซอร์อาจละเว้นแอตทริบิวต์นี้โดยสิ้นเชิง |
การดึงข้อมูลทรัพยากรจะเริ่มขึ้นเมื่อเอกสาร HTML โหลดและแยกวิเคราะห์เสร็จสมบูรณ์ | ||
ส่วนขยายแหล่งที่มาของสื่อ (MSE) จะไม่สนใจแอตทริบิวต์ preload ในองค์ประกอบสื่อ เนื่องจากแอปมีหน้าที่รับผิดชอบในการส่งสื่อไปยัง MSE
|
||
การโหลดลิงก์ล่วงหน้า |
บังคับให้เบราว์เซอร์ส่งคำขอทรัพยากรวิดีโอโดยไม่บล็อกเหตุการณ์ onload ของเอกสาร
|
คำขอช่วง HTTP ใช้ร่วมกันไม่ได้ |
ใช้งานร่วมกับ MSE และกลุ่มไฟล์ได้ | ควรใช้กับไฟล์สื่อขนาดเล็ก (<5 MB) เมื่อดึงข้อมูลทรัพยากรแบบเต็มเท่านั้น | |
การบัฟเฟอร์ด้วยตนเอง | ควบคุมได้เต็มรูปแบบ | การจัดการข้อผิดพลาดที่ซับซ้อนเป็นความรับผิดชอบของเว็บไซต์ |
แอตทริบิวต์การโหลดวิดีโอล่วงหน้า
หากแหล่งที่มาของวิดีโอเป็นไฟล์ที่ไม่ซ้ำกันซึ่งโฮสต์ในเว็บเซิร์ฟเวอร์ คุณอาจต้องใช้แอตทริบิวต์ preload
ของวิดีโอเพื่อบอกเป็นคำแนะนำแก่เบราว์เซอร์เกี่ยวกับปริมาณข้อมูลหรือเนื้อหาที่จะโหลดล่วงหน้า ซึ่งหมายความว่า Media Source Extensions (MSE) ใช้กับ preload
ไม่ได้
การดึงข้อมูลทรัพยากรจะเริ่มขึ้นก็ต่อเมื่อโหลดและแยกวิเคราะห์เอกสาร HTML เริ่มต้นจนเสร็จสมบูรณ์แล้ว (เช่น มีการเรียกเหตุการณ์ DOMContentLoaded
) ส่วนเหตุการณ์ load
ที่แตกต่างออกไปมากจะเริ่มต้นขึ้นเมื่อมีการดึงข้อมูลทรัพยากรจริงๆ
การตั้งค่าแอตทริบิวต์ preload
เป็น metadata
บ่งบอกว่าผู้ใช้ไม่จําเป็นต้องใช้วิดีโอ แต่การดึงข้อมูลเมตา (มิติข้อมูล รายการแทร็ก ระยะเวลา และอื่นๆ) ของวิดีโอนั้นเป็นสิ่งที่ควรทำ โปรดทราบว่าตั้งแต่ Chrome
64 เป็นต้นไป ค่าเริ่มต้นของ preload
คือ metadata
(ก่อนหน้านี้คือ auto
)
<video id="video" preload="metadata" src="file.mp4" controls></video>
<script>
video.addEventListener('loadedmetadata', function() {
if (video.buffered.length === 0) return;
const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
console.log(`${bufferedSeconds} seconds of video are ready to play.`);
});
</script>
การตั้งค่าแอตทริบิวต์ preload
เป็น auto
บ่งบอกว่าเบราว์เซอร์อาจแคชข้อมูลไว้มากพอที่จะเล่นได้จนจบโดยไม่ต้องหยุดเพื่อบัฟเฟอร์เพิ่มเติม
<video id="video" preload="auto" src="file.mp4" controls></video>
<script>
video.addEventListener('loadedmetadata', function() {
if (video.buffered.length === 0) return;
const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
console.log(`${bufferedSeconds} seconds of video are ready to play.`);
});
</script>
แต่มีข้อควรระวังอยู่บ้าง เนื่องจากนี่เป็นเพียงคำแนะนำ เบราว์เซอร์อาจไม่สนใจแอตทริบิวต์ preload
เลย กฎบางส่วนที่ใช้ใน Chrome ณ เวลาที่เขียนบทความมีดังนี้
- เมื่อเปิดใช้การประหยัดอินเทอร์เน็ต Chrome จะบังคับให้ค่า
preload
เป็นnone
- ใน Android 4.3 นั้น Chrome จะบังคับให้ค่า
preload
เป็นnone
เนื่องจากข้อบกพร่องของ Android - ในการเชื่อมต่อเครือข่ายมือถือ (2G, 3G และ 4G) Chrome จะบังคับให้ค่า
preload
เป็นmetadata
เคล็ดลับ
หากเว็บไซต์มีทรัพยากรวิดีโอจำนวนมากในโดเมนเดียวกัน เราขอแนะนำให้คุณตั้งค่า preload
เป็น metadata
หรือกำหนดแอตทริบิวต์ poster
แล้วตั้งค่า preload
เป็น none
วิธีนี้จะช่วยหลีกเลี่ยงการเชื่อมต่อ HTTP กับโดเมนเดียวกันถึงจำนวนสูงสุด (6 ตามข้อกำหนด HTTP 1.1) ซึ่งอาจทำให้การโหลดทรัพยากรค้าง โปรดทราบว่าการดำเนินการนี้ยังอาจปรับปรุงความเร็วหน้าเว็บได้อีกด้วยหากวิดีโอไม่ได้เป็นส่วนหนึ่งของประสบการณ์หลักของผู้ใช้
การโหลดลิงก์ล่วงหน้า
ตามที่ได้อธิบายไว้ในบทความอื่นๆ การโหลดลิงก์ล่วงหน้าคือการดึงข้อมูลแบบประกาศที่ช่วยให้คุณบังคับเบราว์เซอร์ให้ส่งคำขอทรัพยากรได้โดยไม่ต้องบล็อกเหตุการณ์ load
และขณะที่หน้าเว็บกำลังดาวน์โหลด ทรัพยากรที่โหลดผ่าน <link rel="preload">
จะจัดเก็บไว้ในเครื่องของเบราว์เซอร์ และจะไม่มีผลจนกว่าจะมีการอ้างอิงอย่างชัดเจนใน DOM, JavaScript หรือ CSS
การโหลดล่วงหน้าแตกต่างจากการเรียกข้อมูลล่วงหน้าตรงที่การโหลดล่วงหน้าจะมุ่งเน้นที่การนำทางปัจจุบันและดึงข้อมูลทรัพยากรโดยให้ความสำคัญกับประเภทของทรัพยากร (สคริปต์ สไตล์ แบบอักษร วิดีโอ เสียง ฯลฯ) ควรใช้เพื่ออุ่นแคชเบราว์เซอร์สำหรับเซสชันปัจจุบัน
โหลดวิดีโอแบบเต็มล่วงหน้า
วิธีโหลดวิดีโอแบบเต็มในเว็บไซต์ไว้ล่วงหน้าเพื่อให้เบราว์เซอร์อ่านจากแคชได้เมื่อ JavaScript ขอให้ดึงข้อมูลเนื้อหาวิดีโอ เนื่องจากเบราว์เซอร์อาจแคชทรัพยากรไว้แล้ว หากคำขอโหลดล่วงหน้ายังไม่เสร็จสิ้น ระบบจะดึงข้อมูลเครือข่ายตามปกติ
<link rel="preload" as="video" href="https://cdn.com/small-file.mp4">
<video id="video" controls></video>
<script>
// Later on, after some condition has been met, set video source to the
// preloaded video URL.
video.src = 'https://cdn.com/small-file.mp4';
video.play().then(() => {
// If preloaded video URL was already cached, playback started immediately.
});
</script>
เนื่องจากองค์ประกอบวิดีโอในตัวอย่างจะใช้ทรัพยากรที่โหลดไว้ล่วงหน้า ค่าของลิงก์การโหลดล่วงหน้า as
คือ video
หากเป็นองค์ประกอบเสียง จะเป็น as="audio"
โหลดกลุ่มแรกล่วงหน้า
ตัวอย่างด้านล่างแสดงวิธีโหลดส่วนแรกของวิดีโอล่วงหน้าด้วย <link
rel="preload">
และใช้กับส่วนขยายแหล่งที่มาของสื่อ หากไม่คุ้นเคยกับ MSE JavaScript API โปรดดูข้อมูลเบื้องต้นเกี่ยวกับ MSE
เพื่อความง่าย เราจะสมมติว่าวิดีโอทั้งหมดถูกแยกออกเป็นไฟล์ขนาดเล็กๆ เช่น file_1.webm
, file_2.webm
, file_3.webm
ฯลฯ
<link rel="preload" as="fetch" href="https://cdn.com/file_1.webm">
<video id="video" controls></video>
<script>
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });
function sourceOpen() {
URL.revokeObjectURL(video.src);
const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
// If video is preloaded already, fetch will return immediately a response
// from the browser cache (memory cache). Otherwise, it will perform a
// regular network fetch.
fetch('https://cdn.com/file_1.webm')
.then(response => response.arrayBuffer())
.then(data => {
// Append the data into the new sourceBuffer.
sourceBuffer.appendBuffer(data);
// TODO: Fetch file_2.webm when user starts playing video.
})
.catch(error => {
// TODO: Show "Video is not available" message to user.
});
}
</script>
การสนับสนุน
คุณสามารถตรวจหาการรองรับ as
ประเภทต่างๆ สำหรับ <link rel=preload>
ได้ด้วยข้อมูลโค้ดด้านล่าง
function preloadFullVideoSupported() {
const link = document.createElement('link');
link.as = 'video';
return (link.as === 'video');
}
function preloadFirstSegmentSupported() {
const link = document.createElement('link');
link.as = 'fetch';
return (link.as === 'fetch');
}
การบัฟเฟอร์ด้วยตนเอง
ก่อนจะเจาะลึกเรื่อง Cache API และ Service Worker เรามาดูวิธีบัฟเฟอร์วิดีโอด้วย MSE ด้วยตนเองกัน ตัวอย่างด้านล่างนี้ถือว่าเว็บเซิร์ฟเวอร์ของคุณรองรับคำขอ HTTP Range
แต่การดำเนินการนี้จะคล้ายกับกลุ่มไฟล์ โปรดทราบว่าไลบรารีมิดเดิลแวร์บางรายการ เช่น Shaka Player ของ Google, JW Player และ Video.js สร้างขึ้นเพื่อจัดการเรื่องนี้ให้คุณ
<video id="video" controls></video>
<script>
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });
function sourceOpen() {
URL.revokeObjectURL(video.src);
const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
// Fetch beginning of the video by setting the Range HTTP request header.
fetch('file.webm', { headers: { range: 'bytes=0-567139' } })
.then(response => response.arrayBuffer())
.then(data => {
sourceBuffer.appendBuffer(data);
sourceBuffer.addEventListener('updateend', updateEnd, { once: true });
});
}
function updateEnd() {
// Video is now ready to play!
const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
console.log(`${bufferedSeconds} seconds of video are ready to play.`);
// Fetch the next segment of video when user starts playing the video.
video.addEventListener('playing', fetchNextSegment, { once: true });
}
function fetchNextSegment() {
fetch('file.webm', { headers: { range: 'bytes=567140-1196488' } })
.then(response => response.arrayBuffer())
.then(data => {
const sourceBuffer = mediaSource.sourceBuffers[0];
sourceBuffer.appendBuffer(data);
// TODO: Fetch further segment and append it.
});
}
</script>
ข้อควรพิจารณา
เมื่อคุณควบคุมประสบการณ์การบัฟเฟอร์สื่อทั้งหมดได้แล้ว เราขอแนะนำให้พิจารณาระดับแบตเตอรี่ของอุปกรณ์ ความชอบของผู้ใช้ "โหมดประหยัดอินเทอร์เน็ต" และข้อมูลเครือข่ายเมื่อพิจารณาเกี่ยวกับการโหลดล่วงหน้า
การตรวจหาแบตเตอรี่
พิจารณาระดับแบตเตอรี่ของอุปกรณ์ของผู้ใช้ก่อนตัดสินใจโหลดวิดีโอล่วงหน้า ซึ่งจะช่วยประหยัดอายุการใช้งานแบตเตอรี่เมื่อระดับพลังงานต่ำ
ปิดใช้การโหลดล่วงหน้า หรืออย่างน้อยก็โหลดวิดีโอที่มีความละเอียดต่ำไว้ล่วงหน้าเมื่อแบตเตอรี่ของอุปกรณ์เหลือน้อย
if ('getBattery' in navigator) {
navigator.getBattery()
.then(battery => {
// If battery is charging or battery level is high enough
if (battery.charging || battery.level > 0.15) {
// TODO: Preload the first segment of a video.
}
});
}
ตรวจหา "โปรแกรมประหยัดอินเทอร์เน็ต"
ใช้ส่วนหัวคำขอคำแนะนำไคลเอ็นต์ Save-Data
เพื่อแสดงแอปพลิเคชันที่รวดเร็วและเบาแก่ผู้ใช้ที่เลือกใช้โหมด "ประหยัดอินเทอร์เน็ต" ในเบราว์เซอร์ การระบุส่วนหัวของคำขอนี้จะช่วยให้แอปพลิเคชันปรับแต่งและมอบประสบการณ์การใช้งานที่เพิ่มประสิทธิภาพให้แก่ผู้ใช้ที่มีข้อจำกัดด้านต้นทุนและประสิทธิภาพได้
ดูข้อมูลเพิ่มเติมที่การนำเสนอแอปพลิเคชันที่รวดเร็วและเบาด้วย "ประหยัดอินเทอร์เน็ต"
การโหลดที่ชาญฉลาดตามข้อมูลเครือข่าย
คุณอาจต้องตรวจสอบ navigator.connection.type
ก่อนโหลดล่วงหน้า เมื่อตั้งค่าเป็น cellular
คุณจะป้องกันไม่ให้ระบบโหลดล่วงหน้าและแจ้งให้ผู้ใช้ทราบว่าผู้ให้บริการเครือข่ายมือถืออาจเรียกเก็บค่าแบนด์วิดท์ และเริ่มเล่นเนื้อหาที่แคชไว้ก่อนหน้านี้โดยอัตโนมัติเท่านั้น
if ('connection' in navigator) {
if (navigator.connection.type == 'cellular') {
// TODO: Prompt user before preloading video
} else {
// TODO: Preload the first segment of a video.
}
}
ดูตัวอย่างข้อมูลเครือข่ายเพื่อดูวิธีตอบสนองต่อการเปลี่ยนแปลงของเครือข่าย
แคชกลุ่มแรกหลายกลุ่มไว้ล่วงหน้า
ในกรณีนี้ ฉันจะโหลดเนื้อหาสื่อบางส่วนไว้ล่วงหน้าโดยประมาณได้อย่างไรโดยไม่ทราบว่าผู้ใช้จะเลือกเนื้อหาใด หากผู้ใช้อยู่ในหน้าเว็บที่มีวิดีโอ 10 รายการ เราอาจมีหน่วยความจำเพียงพอที่จะดึงข้อมูลไฟล์กลุ่มจากแต่ละรายการได้ แต่ไม่ควรสร้างองค์ประกอบ <video>
ที่ซ่อนเร้นและออบเจ็กต์ MediaSource
10 รายการ แล้วเริ่มป้อนข้อมูลนั้น
ตัวอย่างแบบ 2 ส่วนด้านล่างแสดงวิธีแคชช่วงแรกของวิดีโอหลายช่วงไว้ล่วงหน้าโดยใช้ Cache API ที่มีประสิทธิภาพและใช้งานง่าย โปรดทราบว่าคุณทำสิ่งคล้ายๆ นี้ได้โดยใช้ IndexedDB เช่นกัน เรายังไม่ใช้ Service Worker เนื่องจากเข้าถึง Cache API จากออบเจ็กต์ window
ได้ด้วย
ดึงข้อมูลและแคช
const videoFileUrls = [
'bat_video_file_1.webm',
'cow_video_file_1.webm',
'dog_video_file_1.webm',
'fox_video_file_1.webm',
];
// Let's create a video pre-cache and store all first segments of videos inside.
window.caches.open('video-pre-cache')
.then(cache => Promise.all(videoFileUrls.map(videoFileUrl => fetchAndCache(videoFileUrl, cache))));
function fetchAndCache(videoFileUrl, cache) {
// Check first if video is in the cache.
return cache.match(videoFileUrl)
.then(cacheResponse => {
// Let's return cached response if video is already in the cache.
if (cacheResponse) {
return cacheResponse;
}
// Otherwise, fetch the video from the network.
return fetch(videoFileUrl)
.then(networkResponse => {
// Add the response to the cache and return network response in parallel.
cache.put(videoFileUrl, networkResponse.clone());
return networkResponse;
});
});
}
โปรดทราบว่าหากฉันใช้คําขอ Range
ของ HTTP ฉันจะต้องสร้างออบเจ็กต์ Response
อีกครั้งด้วยตนเอง เนื่องจาก Cache API ยังไม่รองรับการตอบกลับ Range
โปรดทราบว่าการเรียกใช้ networkResponse.arrayBuffer()
จะดึงข้อมูลเนื้อหาทั้งหมดของคำตอบลงในหน่วยความจำของโปรแกรมแสดงผลพร้อมกัน คุณจึงอาจต้องใช้ช่วงเล็กๆ
เราได้แก้ไขตัวอย่างข้างต้นบางส่วนเพื่อบันทึกคำขอ HTTP Range ไว้สำหรับการแคชวิดีโอล่วงหน้าเพื่อเป็นข้อมูลอ้างอิง
...
return fetch(videoFileUrl, { headers: { range: 'bytes=0-567139' } })
.then(networkResponse => networkResponse.arrayBuffer())
.then(data => {
const response = new Response(data);
// Add the response to the cache and return network response in parallel.
cache.put(videoFileUrl, response.clone());
return response;
});
เล่นวิดีโอ
เมื่อผู้ใช้คลิกปุ่มเล่น เราจะดึงข้อมูลวิดีโอส่วนแรกที่มีอยู่ใน Cache API เพื่อให้การเล่นเริ่มขึ้นทันที (หากมี) ไม่เช่นนั้น เราจะดึงข้อมูลจากเครือข่าย โปรดทราบว่าเบราว์เซอร์และผู้ใช้อาจเลือกล้างแคช
ดังที่กล่าวไว้ก่อนหน้านี้ เราใช้ MSE เพื่อส่งข้อมูลวิดีโอส่วนแรกไปยังองค์ประกอบวิดีโอ
function onPlayButtonClick(videoFileUrl) {
video.load(); // Used to be able to play video later.
window.caches.open('video-pre-cache')
.then(cache => fetchAndCache(videoFileUrl, cache)) // Defined above.
.then(response => response.arrayBuffer())
.then(data => {
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });
function sourceOpen() {
URL.revokeObjectURL(video.src);
const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
sourceBuffer.appendBuffer(data);
video.play().then(() => {
// TODO: Fetch the rest of the video when user starts playing video.
});
}
});
}
สร้างการตอบกลับแบบช่วงด้วย Service Worker
จะเกิดอะไรขึ้นหากคุณดึงข้อมูลไฟล์วิดีโอทั้งไฟล์และบันทึกไว้ใน Cache API เมื่อเบราว์เซอร์ส่งคำขอ Range
HTTP คุณคงไม่ต้องการนำวิดีโอทั้งเรื่องไปไว้ในหน่วยความจำของโปรแกรมแสดงผล เนื่องจาก Cache API ยังไม่รองรับการตอบกลับ Range
เราขอแสดงวิธีสกัดกั้นคําขอเหล่านี้และแสดงRange
ตอบกลับที่ปรับแต่งจาก Service Worker
addEventListener('fetch', event => {
event.respondWith(loadFromCacheOrFetch(event.request));
});
function loadFromCacheOrFetch(request) {
// Search through all available caches for this request.
return caches.match(request)
.then(response => {
// Fetch from network if it's not already in the cache.
if (!response) {
return fetch(request);
// Note that we may want to add the response to the cache and return
// network response in parallel as well.
}
// Browser sends a HTTP Range request. Let's provide one reconstructed
// manually from the cache.
if (request.headers.has('range')) {
return response.blob()
.then(data => {
// Get start position from Range request header.
const pos = Number(/^bytes\=(\d+)\-/g.exec(request.headers.get('range'))[1]);
const options = {
status: 206,
statusText: 'Partial Content',
headers: response.headers
}
const slicedResponse = new Response(data.slice(pos), options);
slicedResponse.setHeaders('Content-Range': 'bytes ' + pos + '-' +
(data.size - 1) + '/' + data.size);
slicedResponse.setHeaders('X-From-Cache': 'true');
return slicedResponse;
});
}
return response;
}
}
โปรดทราบว่าเราใช้ response.blob()
เพื่อสร้างคำตอบที่แบ่งส่วนนี้ขึ้นมาใหม่ เนื่องจากการดำเนินการนี้เป็นเพียงการให้แฮนเดิลสำหรับไฟล์ ขณะที่ response.arrayBuffer()
จะนําทั้งไฟล์ไปไว้ในหน่วยความจําของโปรแกรมแสดงผล
ส่วนหัว HTTP ของ X-From-Cache
ที่กําหนดเองสามารถใช้เพื่อดูว่าคําขอนี้มาจากแคชหรือมาจากเครือข่าย ผู้เล่น เช่น ShakaPlayer สามารถใช้เพื่อละเว้นเวลาในการตอบสนองในฐานะตัวบ่งชี้ความเร็วของเครือข่าย
โปรดดูตัวอย่างแอปสื่ออย่างเป็นทางการและไฟล์ ranged-response.js โดยเฉพาะเพื่อดูวิธีจัดการRange
คำขออย่างครบวงจร