리소스를 적극적으로 미리 로드하여 미디어 재생 속도를 높이는 방법
재생 시작 속도가 빠르면 더 많은 사용자가 동영상을 시청하거나 오디오를 청취합니다. 알려진 사실입니다. 이 도움말에서는 사용 사례에 따라 리소스를 적극적으로 미리 로드하여 오디오 및 동영상 재생의 속도를 높이는 데 사용할 수 있는 기법을 살펴봅니다.
미디어 파일을 미리 로드하는 세 가지 방법을 먼저 설명하겠습니다. 먼저 각각의 장단점이 있습니다.
멋진데... | 하지만... | |
---|---|---|
동영상 미리 로드 속성 | 웹 서버에서 호스팅되는 고유한 파일에 간편하게 사용할 수 있습니다. | 브라우저는 이 속성을 완전히 무시할 수 있습니다. |
리소스 가져오기는 HTML 문서가 완전히 로드되고 파싱되면 시작됩니다. | ||
Media Source Extensions(MSE)는 앱이 MSE에 미디어를 제공할 책임이 있으므로 미디어 요소의 preload 속성을 무시합니다.
|
||
링크 미리 로드 |
브라우저가 문서의 onload 이벤트를 차단하지 않고 동영상 리소스를 요청하도록 강제합니다.
|
HTTP 범위 요청은 호환되지 않습니다. |
MSE 및 파일 세그먼트와 호환됩니다. | 전체 리소스를 가져올 때 작은 미디어 파일(5MB 미만)에만 사용해야 합니다. | |
수동 버퍼링 | 전체 제어 | 복잡한 오류 처리는 웹사이트의 책임입니다. |
동영상 미리 로드 속성
동영상 소스가 웹 서버에 호스팅된 고유한 파일인 경우 동영상 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은 Android 버그로 인해
preload
값을none
로 강제합니다. - 모바일 데이터 연결 (2G, 3G, 4G)에서 Chrome은
preload
값을metadata
로 강제 적용합니다.
팁
웹사이트에 동일한 도메인의 동영상 리소스가 많은 경우 preload
값을 metadata
로 설정하거나 poster
속성을 정의하고 preload
를 none
로 설정하는 것이 좋습니다. 이렇게 하면 동일한 도메인에 대한 최대 HTTP 연결 수 (HTTP 1.1 사양에 따라 6개)에 도달하지 않아 리소스 로드가 지연될 수 있습니다. 동영상이 핵심 사용자 환경에 포함되지 않은 경우 페이지 속도도 개선될 수 있습니다.
링크 미리 로드
다른 기사에서 다루었듯이 link preload는 선언적 가져오기로, 이를 통해 브라우저에서 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>
지원
아래 스니펫을 사용하여 <link rel=preload>
의 다양한 as
유형 지원을 감지할 수 있습니다.
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와 서비스 워커에 대해 알아보기 전에 MSE를 사용하여 동영상을 수동으로 버퍼링하는 방법을 알아보겠습니다. 아래 예에서는 웹 서버가 HTTP Range
요청을 지원한다고 가정하지만 파일 세그먼트와 매우 유사합니다. Google의 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개를 만들고 이 데이터를 제공해서는 안 됩니다.
아래의 두 부분으로 구성된 예에서는 강력하고 사용하기 쉬운 Cache API를 사용하여 동영상의 첫 번째 세그먼트를 여러 개 사전 캐시하는 방법을 보여줍니다. IndexedDB로도 비슷한 작업을 수행할 수 있습니다. 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
요청을 사용하려면 Cache API가 아직 Range
응답을 지원하지 않으므로 Response
객체를 수동으로 다시 만들어야 합니다. networkResponse.arrayBuffer()
를 호출하면 응답의 전체 콘텐츠를 한 번에 렌더기 메모리로 가져오므로 작은 범위를 사용하는 것이 좋습니다.
참고로 위 예시의 일부를 수정하여 HTTP 범위 요청을 동영상 사전 캐시에 저장했습니다.
...
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.
});
}
});
}
서비스 워커로 범위 응답 만들기
이제 전체 동영상 파일을 가져와서 Cache API에 저장한 경우에는 어떻게 해야 할까요? 브라우저가 HTTP Range
요청을 전송할 때 Cache API가 Range
응답을 아직 지원하지 않으므로 전체 동영상을 렌더러 메모리로 가져오면 안 됩니다.
이러한 요청을 가로채고 서비스 워커에서 맞춤설정된 Range
응답을 반환하는 방법을 보여드리겠습니다.
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.blob()
는 파일 핸들만 제공하는 반면 response.arrayBuffer()
는 전체 파일을 렌더러 메모리로 가져옵니다.
맞춤 X-From-Cache
HTTP 헤더를 사용하여 이 요청이 캐시에서 온 것인지 네트워크에서 온 것인지 알 수 있습니다. ShakaPlayer와 같은 플레이어에서 응답 시간을 네트워크 속도의 지표로 무시하는 데 사용할 수 있습니다.
Range
요청을 처리하는 방법에 관한 전체 솔루션은 공식 샘플 미디어 앱, 특히 ranged-response.js 파일을 참고하세요.