วิธีเร่งการเล่นสื่อด้วยการโหลดทรัพยากรล่วงหน้าอย่างสม่ำเสมอ
การเริ่มเล่นที่เร็วขึ้นหมายความว่ามีผู้คนจำนวนมากขึ้นที่ดูวิดีโอหรือฟังเพลง เสียง เป็นข้อเท็จจริงที่รู้อยู่แล้ว ในบทความนี้ เราจะพูดถึง เทคนิคที่คุณสามารถใช้เพื่อเร่งการเล่นเสียงและวิดีโอ ด้วยการ การโหลดทรัพยากรล่วงหน้าโดยขึ้นอยู่กับกรณีการใช้งาน
ฉันจะอธิบายวิธีการโหลดไฟล์สื่อล่วงหน้า 3 วิธี โดยเริ่มจากมืออาชีพ และข้อเสีย
เยี่ยมมาก... | แต่ว่า... | |
---|---|---|
แอตทริบิวต์การโหลดวิดีโอล่วงหน้า | ใช้งานง่ายสำหรับไฟล์ที่ไม่ซ้ำกันที่โฮสต์ในเว็บเซิร์ฟเวอร์ | เบราว์เซอร์อาจละเว้นแอตทริบิวต์นี้โดยสิ้นเชิง |
การดึงข้อมูลทรัพยากรจะเริ่มต้นเมื่อโหลดเอกสาร HTML เสร็จสมบูรณ์ และ แยกวิเคราะห์แล้ว | ||
ส่วนขยายแหล่งที่มาของสื่อ (MSE) จะไม่สนใจแอตทริบิวต์ preload ในองค์ประกอบสื่อเนื่องจากแอปเป็นผู้รับผิดชอบ
การจัดหาสื่อให้กับ MSE
|
||
การโหลดลิงก์ล่วงหน้า |
บังคับให้เบราว์เซอร์ส่งคำขอทรัพยากรวิดีโอโดยไม่บล็อก
เหตุการณ์ onload ของเอกสาร
|
คำขอช่วง HTTP ใช้ร่วมกันไม่ได้ |
เข้ากันได้กับ MSE และกลุ่มไฟล์ | ควรใช้สำหรับไฟล์สื่อขนาดเล็ก (<5 MB) เมื่อดึงข้อมูลทรัพยากรทั้งหมดเท่านั้น | |
การบัฟเฟอร์ด้วยตนเอง | ควบคุมได้เต็มรูปแบบ | การจัดการข้อผิดพลาดที่ซับซ้อนถือเป็นความรับผิดชอบของเว็บไซต์ |
แอตทริบิวต์การโหลดวิดีโอล่วงหน้า
หากแหล่งที่มาของวิดีโอเป็นไฟล์ที่ไม่ซ้ำกันที่โฮสต์บนเว็บเซิร์ฟเวอร์ คุณอาจต้องการทำดังนี้
ใช้แอตทริบิวต์วิดีโอ preload
เพื่อให้คำแนะนำแก่เบราว์เซอร์ว่าวิธีการ
ข้อมูลหรือเนื้อหามากมายที่จะโหลดไว้ล่วงหน้า ซึ่งหมายความว่า ส่วนขยายแหล่งที่มาของสื่อ
(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
แต่นี่จะค่อนข้างคล้ายกันกับ
กลุ่ม โปรดทราบว่าไลบรารีมิดเดิลแวร์บางส่วน เช่น Google's Shaka
Player, 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>
ที่ซ่อนไว้ 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;
});
});
}
โปรดทราบว่าหากฉันจะใช้คำขอ HTTP Range
ฉันจะต้องสร้างใหม่ด้วยตนเอง
อ็อบเจ็กต์ 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;
});
เล่นวิดีโอ
เมื่อผู้ใช้คลิกปุ่มเล่น เราจะดึงข้อมูลส่วนแรกของวิดีโอ ที่มีอยู่ใน 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
หากคุณได้ดึงข้อมูลไฟล์วิดีโอทั้งไฟล์และบันทึกไว้ใน
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 เพื่อละเว้นเวลาตอบสนองซึ่งเป็นตัวบ่งชี้
ความเร็วเครือข่าย
ดูตัวอย่าง แอปสื่อตัวอย่าง อย่างเป็นทางการ และโดยเฉพาะอย่างยิ่ง
ranged-response.js สำหรับโซลูชันครบวงจรสำหรับวิธีจัดการ Range
คำขอ