วิธีเร่งการเล่นสื่อด้วยการโหลดทรัพยากรล่วงหน้าอย่างสม่ำเสมอ
การเริ่มเล่นที่เร็วขึ้นหมายความว่าจะมีผู้คนจำนวนมากขึ้นที่ดูวิดีโอหรือฟังเสียงของคุณ เป็นข้อเท็จจริงที่รู้อยู่แล้ว ในบทความนี้ ฉันจะสำรวจเทคนิคที่คุณสามารถใช้เพื่อเร่งการเล่นเสียงและวิดีโอด้วยการโหลดทรัพยากรล่วงหน้าอย่างต่อเนื่อง ทั้งนี้ขึ้นอยู่กับกรณีการใช้งานของคุณ
ผมจะอธิบายวิธีการโหลดไฟล์สื่อล่วงหน้า 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 รายการ เราอาจจะมีหน่วยความจำเพียงพอที่จะดึงไฟล์กลุ่ม 1 ไฟล์จากแต่ละไฟล์ แต่เราไม่ควรสร้างเอลิเมนต์ <video>
ที่ซ่อนไว้ 10 รายการและออบเจ็กต์ 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 เมื่อเบราว์เซอร์ส่งคำขอ HTTP Range
คุณคงไม่ต้องการนำทั้งวิดีโอไปไว้ในหน่วยความจำของการแสดงผล เนื่องจาก 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 เพื่อละเว้นเวลาตอบสนองเป็นตัวบ่งชี้ความเร็วเครือข่าย
ลองดู Sample Media App อย่างเป็นทางการและโดยเฉพาะอย่างยิ่งไฟล์ ranged-response.js เพื่อดูโซลูชันที่สมบูรณ์สำหรับวิธีจัดการคำขอ Range