Kaynakları etkin bir şekilde önceden yükleyerek medya oynatma işlemlerinizi nasıl hızlandırabilirsiniz?
Oynatmanın daha hızlı başlatılması, daha fazla kişinin videonuzu izlemesi veya sesinizi dinlemesi anlamına gelir. Bu bilinen bir gerçektir. Bu makalede, kullanım alanınıza bağlı olarak kaynakları etkin bir şekilde önceden yükleyerek ses ve video oynatma hızınızı artırmak için kullanabileceğiniz teknikleri inceleyeceğiz.
Medya dosyalarını önceden yüklemeyle ilgili üç yöntemi, avantaj ve dezavantajlarıyla birlikte açıklayacağım.
Harika... | Ama... | |
---|---|---|
Videoyu önceden yükleme özelliği | Web sunucusunda barındırılan benzersiz bir dosya için kullanımı kolaydır. | Tarayıcılar bu özelliği tamamen yoksayabilir. |
Kaynak getirme, HTML belgesi tamamen yüklenip ayrıştırıldığında başlar. | ||
Medya Kaynağı Uzantıları (MSE), MSE'ye medya sağlamaktan uygulama sorumlu olduğu için medya öğelerindeki preload özelliğini yoksayar.
|
||
Bağlantıyı önceden yükleme |
Tarayıcıyı, dokümanın onload etkinliğini engellemeden bir video kaynağı için istekte bulunmaya zorlar.
|
HTTP Aralığı istekleri uyumlu değildir. |
MSE ve dosya segmentleriyle uyumludur. | Tam kaynaklar alınırken yalnızca küçük medya dosyaları (5 MB'tan küçük) için kullanılmalıdır. | |
Manuel arabelleğe alma | Tam kontrol | Karmaşık hatalarla ilgili işlemler web sitesinin sorumluluğundadır. |
Videonun önceden yüklenmesi özelliği
Video kaynağı bir web sunucusunda barındırılan benzersiz bir dosyaysa önceden ne kadar bilgi veya içerik yükleneceği konusunda tarayıcıya ipucu vermek için video preload
özelliğini kullanabilirsiniz. Bu, Medya Kaynağı Uzantıları (MSE)'nın preload
ile uyumlu olmadığı anlamına gelir.
Kaynak getirme işlemi yalnızca ilk HTML belgesi tamamen yüklenip ayrıştırıldığında (ör.DOMContentLoaded
etkinliği tetiklendiğinde) başlar. Çok farklı load
etkinliği ise kaynak getirildiğinde tetiklenir.
preload
özelliğinin metadata
olarak ayarlanması, kullanıcının videoya ihtiyaç duymasının beklenmediğini, ancak meta verilerinin (boyutlar, parça listesi, süre vb.) getirilmesinin istendiğini gösterir. Chrome 64'ten itibaren preload
için varsayılan değerin metadata
olduğunu unutmayın. (Daha önce auto
idi).
<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
özelliğinin auto
olarak ayarlanması, tarayıcının daha fazla arabelleğe alma için durdurmaya gerek kalmadan oynatmanın tamamlandığı yeterli veriyi önbelleğe alabileceği anlamına gelir.
<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>
Ancak bazı noktalara dikkat etmeniz gerekir. Bu yalnızca bir ipucu olduğundan tarayıcı, preload
özelliğini tamamen yoksayabilir. Bu yazının yazıldığı sırada Chrome'da uygulanan bazı kurallar aşağıda verilmiştir:
- Veri Tasarrufu etkinleştirildiğinde Chrome,
preload
değerininone
değerine zorlar. - Android 4.3'te Chrome, bir Android Hatası nedeniyle
preload
değerininone
olarak zorunlu kılar. - Chrome, hücresel bağlantılarda (2G, 3G ve 4G)
preload
değerinimetadata
olmaya zorlar.
İpuçları
Web siteniz aynı alanda çok sayıda video kaynağı içeriyorsa preload
değerini metadata
olarak ayarlamanızı veya poster
özelliğini tanımlayıp preload
değerini none
olarak ayarlamanızı öneririz. Bu sayede, aynı alan adıyla maksimum HTTP bağlantısı sayısına (HTTP 1.1 spesifikasyonuna göre 6) ulaşarak kaynakların yüklenmesini askıya alabilirsiniz. Videolar temel kullanıcı deneyiminizin bir parçası değilse bu işlemin sayfa hızını da iyileştirebileceğini unutmayın.
Bağlantıyı önceden yükleme
Diğer makalelerde anlaşıldığı üzere bağlantı önceden yükleme, load
etkinliğini engellemeden ve sayfa indirilirken tarayıcıyı bir kaynak için istekte bulunmaya zorlamanızı sağlayan bildirimli bir getirme işlemidir. <link rel="preload">
aracılığıyla yüklenen kaynaklar, tarayıcıda yerel olarak depolanır ve DOM, JavaScript veya CSS'de açıkça referans verilene kadar etkili şekilde etkisizdir.
Önceden yükleme, mevcut gezinmeye odaklandığı ve kaynakları türlerine (komut dosyası, stil, yazı tipi, video, ses vb.) göre öncelikli olarak getirdiği için ön getirmeden farklıdır. Mevcut oturumlar için tarayıcı önbelleğini ısıtmak amacıyla kullanılmalıdır.
Videonun tamamını önceden yükle
Web sitenizde bir videonun tamamını ön yüklemek için aşağıdaki adımları uygulayın. Böylece, JavaScript'iniz video içeriğini getirmeyi istediğinde kaynak tarayıcı tarafından önbelleğe alınmış olabileceğinden önbellekten okunur. Önceden yükleme isteği henüz bitmemişse normal bir ağ getirme işlemi gerçekleşir.
<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>
Önceden yüklenmiş kaynak, örnekteki bir video öğesi tarafından tüketileceği için as
önceden yükleme bağlantısının değeri video
olur. Bir ses öğesi olsaydı as="audio"
olacaktı.
İlk segmenti önceden yükleme
Aşağıdaki örnekte, bir videonun ilk segmentinin <link
rel="preload">
ile nasıl önceden yükleneceği ve Medya Kaynağı Uzantıları ile nasıl kullanılacağı gösterilmektedir. MSE JavaScript API hakkında bilgi sahibi değilseniz MSE ile ilgili temel bilgileri inceleyin.
Basitlik açısından, videonun tamamının file_1.webm
, file_2.webm
, file_3.webm
gibi daha küçük dosyalara bölündüğünü varsayalım.
<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>
Destek
Aşağıdaki snippet'lerle <link rel=preload>
için çeşitli as
türlerinin desteklendiğini tespit edebilirsiniz:
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');
}
Manuel arabelleğe alma
Cache API'ye ve hizmet çalışanlarına geçmeden önce bir videonun MSE ile manuel olarak nasıl arabelleğe alınacağını görelim. Aşağıdaki örnekte, web sunucunuzun HTTP Range
isteklerini desteklediği varsayılmıştır, ancak bu durum dosya segmentlerine oldukça benzer olacaktır. Google ShakaPlayer, JW Player ve Video.js gibi bazı ara yazılım kitaplıklarının bu işlevi size sağlayacak şekilde oluşturulduğunu unutmayın.
<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>
Dikkat edilmesi gereken noktalar
Artık medya arabelleğe alma deneyiminin tamamının kontrolünü ele aldığınız için ön yükleme yaparken cihazın pil seviyesini, "Veri Tasarrufu Modu" kullanıcı tercihini ve ağ bilgilerini göz önünde bulundurmanızı öneririz.
Pil farkındalığı
Bir videoyu önceden yüklemeyi düşünmeden önce kullanıcıların cihazlarının pil seviyesini göz önünde bulundurun. Bu, güç seviyesi düşük olduğunda pil ömrünü korur.
Cihazın pili biterken önceden yüklemeyi devre dışı bırakın veya en azından daha düşük çözünürlüklü bir videoyu önceden yükleyin.
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.
}
});
}
"Veri Tasarrufu"nu algılama
Tarayıcılarında "veri tasarrufu" moduna kaydolan kullanıcılara hızlı ve hafif uygulamalar sunmak için Save-Data
istemci ipucu istek başlığını kullanın. Uygulamanız bu istek başlığını tanımlayarak maliyet ve performans açısından kısıtlanmış kullanıcılara özelleştirilmiş ve optimize edilmiş bir kullanıcı deneyimi sunabilir.
Daha fazla bilgi edinmek için Save-Data ile Hızlı ve Hafif Uygulamalar Yayınlama bölümüne bakın.
Ağ bilgilerine dayalı akıllı yükleme
Önceden yükleme işleminden önce navigator.connection.type
sayfasını kontrol etmenizi öneririz. Politika cellular
olarak ayarlandığında önceden yüklemeyi engelleyebilir ve kullanıcılara mobil ağ operatörlerinin bant genişliği için ücret alıyor olabileceğini söyleyebilir ve yalnızca önceden önbelleğe alınmış içeriğin otomatik olarak oynatılmasını başlatabilirsiniz.
if ('connection' in navigator) {
if (navigator.connection.type == 'cellular') {
// TODO: Prompt user before preloading video
} else {
// TODO: Preload the first segment of a video.
}
}
Ağ değişikliklerine nasıl tepki vereceğinizi öğrenmek için Ağ Bilgileri örneğine göz atın.
Birden çok ilk segmenti önbelleğe al
Peki kullanıcının hangi medyayı seçeceğini bilmeden bazı medya içeriklerini tahmini olarak önceden yüklemek istersem ne olur? Kullanıcı 10 video içeren bir web sayfasındaysa muhtemelen her bir segmentten bir segment dosyası getirecek kadar belleğimiz vardır, ancak kesinlikle 10 gizli <video>
öğesi ve 10 MediaSource
nesne oluşturup bu verileri beslemeye başlamamamız gerekir.
Aşağıdaki iki bölümden oluşan örnekte, güçlü ve kullanımı kolay Cache API'yi kullanarak videonun ilk birkaç segmentini nasıl önbelleğe alabileceğinizi görebilirsiniz. Benzer bir işlem IndexedDB ile de elde edilebilir. Cache API'ye window
nesnesinden de erişilebildiği için henüz hizmet çalışanları kullanmıyoruz.
Getirme ve önbelleğe alma
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
istekleri kullanacak olsaydım Cache API'nin Range
yanıtlarını henüz desteklemediği için bir Response
nesnesini manuel olarak yeniden oluşturmam gerektiğini unutmayın. networkResponse.arrayBuffer()
çağrısının, yanıtın tüm içeriğini bir kerede oluşturma belleğine getirdiğini unutmayın. Bu nedenle küçük aralıklar kullanmanız önerilir.
Referans olması amacıyla, yukarıdaki örneğin bir kısmını HTTP Range isteklerini video ön önbelleğe kaydetmek için değiştirdim.
...
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;
});
Videoyu oynat
Kullanıcı bir oynatma düğmesini tıkladığında, oynatma işleminin mümkünse hemen başlaması için Cache API'de bulunan videonun ilk segmentini getiririz. Aksi takdirde, dosyayı ağdan alırız. Tarayıcıların ve kullanıcıların Önbelleği temizlemeye karar verebileceğini unutmayın.
Daha önce görüldüğü gibi, videonun bu ilk segmentini video öğesine aktarmak için MSE'yi kullanıyoruz.
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.
});
}
});
}
Hizmet çalışanı ile aralık yanıtları oluşturma
Peki video dosyasının tamamını getirip Cache API'ye kaydettiyseniz ne olur? Tarayıcı bir HTTP Range
isteği gönderdiğinde Cache API'si Range
yanıtlarını henüz desteklemediğinden videonun tamamını oluşturucu belleğine taşımak istemezsiniz.
Şimdi bu isteklere nasıl müdahale edeceğimi ve hizmet çalışanından özelleştirilmiş Range
yanıtı döndüreceğimi göstereyim.
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;
}
}
Bu dilimlenmiş yanıtı yeniden oluşturmak için response.blob()
kullandığımı belirtmek önemlidir. response.blob()
, dosyanın yalnızca bir adını sağlarken response.arrayBuffer()
dosyanın tamamını oluşturucu belleğine getirir.
Özel X-From-Cache
HTTP başlığım, bu isteğin önbellekten mi yoksa ağdan mı geldiğini öğrenmek için kullanılabilir. ShakaPlayer gibi bir oynatıcı tarafından, ağ hızının göstergesi olarak yanıt süresini yoksaymak için kullanılabilir.
Range
isteklerinin nasıl ele alınacağına dair eksiksiz bir çözüm için resmi Sample Media App'e ve özellikle ranged-response.js dosyasına göz atın.