웹에서 최고의 모바일 미디어 환경을 만드는 방법은 무엇인가요? 참 쉽죠? 이는 모두 사용자 참여도와 웹페이지에서 미디어에 부여하는 중요도에 따라 달라집니다. 동영상이 사용자 방문의 주된 이유라면 몰입도 높은 사용자 환경을 제공하여 사용자의 재참여를 유도해야 한다는 데 모두 동의할 것입니다.
이 도움말에서는 다양한 웹 API를 통해 미디어 환경을 점진적으로 개선하고 몰입도를 높이는 방법을 설명합니다. 따라서 맞춤 컨트롤, 전체 화면, 백그라운드 재생이 포함된 간단한 모바일 플레이어 환경을 빌드해 보겠습니다. 지금 샘플을 사용해 보고 GitHub 저장소에서 코드를 확인할 수 있습니다.
맞춤 컨트롤
보시다시피 미디어 플레이어에 사용할 HTML 레이아웃은 매우 간단합니다. <div>
루트 요소에는 <video>
미디어 요소와 동영상 컨트롤 전용 <div>
하위 요소가 포함되어 있습니다.
나중에 다룰 동영상 컨트롤에는 재생/일시중지 버튼, 전체 화면 버튼, 뒤로 및 앞으로 탐색 버튼, 현재 시간, 시간, 시간 추적을 위한 일부 요소가 포함됩니다.
<div id="videoContainer">
<video id="video" src="file.mp4"></video>
<div id="videoControls"></div>
</div>
동영상 메타데이터 읽기
먼저 동영상 메타데이터가 로드될 때까지 기다려 동영상 길이와 현재 시간을 설정하고 진행률 표시줄을 초기화합니다. secondsToTimeCode()
함수는 내가 작성한 맞춤 유틸리티 함수로, 초 수를 'hh:mm:ss' 형식의 문자열로 변환하며 이 경우 더 적합합니다.
<div id="videoContainer">
<video id="video" src="file.mp4"></video>
<div id="videoControls">
<strong>
<div id="videoCurrentTime"></div>
<div id="videoDuration"></div>
<div id="videoProgressBar"></div>
</strong>
</div>
</div>
video.addEventListener('loadedmetadata', function () {
videoDuration.textContent = secondsToTimeCode(video.duration);
videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
videoProgressBar.style.transform = `scaleX(${
video.currentTime / video.duration
})`;
});
동영상 재생/일시중지
동영상 메타데이터가 로드되었으므로 이제 사용자가 재생 상태에 따라 video.play()
및 video.pause()
를 사용하여 동영상을 재생 및 일시중지할 수 있는 첫 번째 버튼을 추가해 보겠습니다.
<div id="videoContainer">
<video id="video" src="file.mp4"></video>
<div id="videoControls">
<strong><button id="playPauseButton"></button></strong>
<div id="videoCurrentTime"></div>
<div id="videoDuration"></div>
<div id="videoProgressBar"></div>
</div>
</div>
playPauseButton.addEventListener('click', function (event) {
event.stopPropagation();
if (video.paused) {
video.play();
} else {
video.pause();
}
});
click
이벤트 리스너에서 동영상 컨트롤을 조정하는 대신 play
및 pause
동영상 이벤트를 사용합니다. 컨트롤을 이벤트 기반으로 하면 유연성이 향상되며 (나중에 Media Session API에서 확인) 브라우저가 재생에 개입하는 경우 컨트롤을 동기화된 상태로 유지할 수 있습니다.
동영상 재생이 시작되면 버튼 상태가 '일시중지'로 변경되고 동영상 컨트롤이 숨겨집니다. 동영상이 일시중지되면 버튼 상태를 '재생'으로 변경하고 동영상 컨트롤이 표시됩니다.
video.addEventListener('play', function () {
playPauseButton.classList.add('playing');
});
video.addEventListener('pause', function () {
playPauseButton.classList.remove('playing');
});
동영상 currentTime
속성에서 나타내는 시간이 timeupdate
동영상 이벤트를 통해 변경되면 맞춤 컨트롤이 표시되는 경우 해당 컨트롤도 업데이트됩니다.
video.addEventListener('timeupdate', function () {
if (videoControls.classList.contains('visible')) {
videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
videoProgressBar.style.transform = `scaleX(${
video.currentTime / video.duration
})`;
}
});
동영상이 끝나면 버튼 상태를 '재생'으로 변경하고 동영상 currentTime
를 0으로 다시 설정하고 당분간 동영상 컨트롤을 표시합니다. 사용자가 어떤 종류의 '자동재생' 기능을 사용 설정한 경우 다른 동영상을 자동으로 로드하도록 선택할 수도 있습니다.
video.addEventListener('ended', function () {
playPauseButton.classList.remove('playing');
video.currentTime = 0;
});
앞뒤로 탐색
계속해서 사용자가 일부 콘텐츠를 쉽게 건너뛸 수 있도록 '뒤로 탐색' 및 '앞으로 탐색' 버튼을 추가해 보겠습니다.
<div id="videoContainer">
<video id="video" src="file.mp4"></video>
<div id="videoControls">
<button id="playPauseButton"></button>
<strong
><button id="seekForwardButton"></button>
<button id="seekBackwardButton"></button
></strong>
<div id="videoCurrentTime"></div>
<div id="videoDuration"></div>
<div id="videoProgressBar"></div>
</div>
</div>
var skipTime = 10; // Time to skip in seconds
seekForwardButton.addEventListener('click', function (event) {
event.stopPropagation();
video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
});
seekBackwardButton.addEventListener('click', function (event) {
event.stopPropagation();
video.currentTime = Math.max(video.currentTime - skipTime, 0);
});
이전과 마찬가지로 이러한 버튼의 click
이벤트 리스너에서 동영상 스타일을 조정하는 대신 실행된 seeking
및 seeked
동영상 이벤트를 사용하여 동영상 밝기를 조정합니다. 맞춤 seeking
CSS 클래스는 filter: brightness(0);
만큼 간단합니다.
video.addEventListener('seeking', function () {
video.classList.add('seeking');
});
video.addEventListener('seeked', function () {
video.classList.remove('seeking');
});
지금까지 만든 내용은 다음과 같습니다. 다음 섹션에서는 전체 화면 버튼을 구현합니다.
전체 화면
여기서는 여러 웹 API를 활용하여 완벽하고 원활한 전체 화면 환경을 만들어 보겠습니다. 실제 동작을 보려면 샘플을 확인하세요.
물론 모든 기능을 사용할 필요는 없습니다. 적합한 템플릿을 선택하고 조합하여 맞춤 흐름을 만드세요.
자동 전체 화면 방지
iOS에서는 미디어 재생이 시작되면 video
요소가 자동으로 전체 화면 모드로 전환됩니다. YouTube는 모바일 브라우저에서 미디어 환경을 최대한 맞춤설정하고 제어하기 위해 노력하고 있으므로 video
요소의 playsinline
속성을 설정하여 iPhone에서 인라인으로 재생되도록 하고 재생이 시작될 때 전체 화면 모드로 전환되지 않도록 하는 것이 좋습니다. 이는 다른 브라우저에는 부작용이 없습니다.
<div id="videoContainer"></div>
<video id="video" src="file.mp4"></video><strong>playsinline</strong></video>
<div id="videoControls">...</div>
</div>
버튼 클릭 시 전체 화면 전환
이제 자동 전체 화면을 사용하지 않으므로 Fullscreen API를 사용하여 동영상의 전체 화면 모드를 직접 처리해야 합니다. 사용자가 '전체 화면 버튼'을 클릭하면 문서에서 현재 전체 화면 모드를 사용 중인 경우 document.exitFullscreen()
를 사용하여 전체 화면 모드를 종료하겠습니다. 그 외의 경우 사용 가능한 경우 requestFullscreen()
메서드를 사용하여 동영상 컨테이너에서 전체 화면을 요청하거나 iOS에서만 동영상 요소에서 webkitEnterFullscreen()
로 대체합니다.
<div id="videoContainer">
<video id="video" src="file.mp4"></video>
<div id="videoControls">
<button id="playPauseButton"></button>
<button id="seekForwardButton"></button>
<button id="seekBackwardButton"></button>
<strong><button id="fullscreenButton"></button></strong>
<div id="videoCurrentTime"></div>
<div id="videoDuration"></div>
<div id="videoProgressBar"></div>
</div>
</div>
fullscreenButton.addEventListener('click', function (event) {
event.stopPropagation();
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
requestFullscreenVideo();
}
});
function requestFullscreenVideo() {
if (videoContainer.requestFullscreen) {
videoContainer.requestFullscreen();
} else {
video.webkitEnterFullscreen();
}
}
document.addEventListener('fullscreenchange', function () {
fullscreenButton.classList.toggle('active', document.fullscreenElement);
});
화면 방향 변경 시 전체 화면 전환
사용자가 가로 모드로 기기를 회전하면 이를 스마트하게 파악하여 몰입도 높은 환경을 만들기 위해 자동으로 전체 화면을 요청합니다. 이를 위해서는 아직 모든 곳에서 지원되지 않고 당시 일부 브라우저에서 접두사가 추가된 Screen Orientation API가 필요합니다. 따라서 이번 업데이트는 YouTube의 첫 번째 점진적 개선사항입니다.
작동 방식 화면 방향 변경을 감지하는 즉시 브라우저 창이 가로 모드(너비가 높이보다 큼)인 경우 전체 화면을 요청합니다. 그렇지 않다면 전체 화면을 종료하겠습니다. 여기까지입니다.
if ('orientation' in screen) {
screen.orientation.addEventListener('change', function () {
// Let's request fullscreen if user switches device in landscape mode.
if (screen.orientation.type.startsWith('landscape')) {
requestFullscreenVideo();
} else if (document.fullscreenElement) {
document.exitFullscreen();
}
});
}
버튼 클릭 시 가로 모드로 잠금 화면
동영상은 가로 모드에서 더 잘 보일 수 있으므로 사용자가 '전체 화면 버튼'을 클릭하면 화면을 가로 모드로 고정하는 것이 좋습니다. 이전에 사용한 Screen Orientation API와 일부 미디어 쿼리를 결합하여 최상의 환경을 제공할 예정입니다.
가로 모드에서 화면을 잠그는 것은 screen.orientation.lock('landscape')
를 호출하는 것만큼 쉽습니다. 하지만 태블릿 사용자에게는 좋은 환경이 아니므로 기기가 matchMedia('(orientation: portrait)')
로 세로 모드이고 matchMedia('(max-device-width: 768px)')
로 한 손에 들어올 때만 이렇게 해야 합니다.
fullscreenButton.addEventListener('click', function (event) {
event.stopPropagation();
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
requestFullscreenVideo();
<strong>lockScreenInLandscape();</strong>;
}
});
function lockScreenInLandscape() {
if (!('orientation' in screen)) {
return;
}
// Let's force landscape mode only if device is in portrait mode and can be held in one hand.
if (
matchMedia('(orientation: portrait) and (max-device-width: 768px)').matches
) {
screen.orientation.lock('landscape');
}
}
기기 방향 변경 시 화면 잠금 해제
방금 만든 잠금 화면 환경은 화면이 잠겨 있을 때 화면 방향 변경을 수신하지 않으므로 완벽하지는 않습니다.
이 문제를 해결하기 위해 가능한 경우 Device Orientation API를 사용하겠습니다. 이 API는 공간에서 기기의 위치와 모션을 측정하는 하드웨어로부터 방향을 위한 자이로스코프와 디지털 나침반과 속도에 관한 가속도계 정보를 제공합니다. 기기 방향 변경을 감지하면 사용자가 기기를 세로 모드로 잡고 화면이 가로 모드로 잠겨 있으면 screen.orientation.unlock()
를 사용하여 화면 잠금을 해제합니다.
function lockScreenInLandscape() {
if (!('orientation' in screen)) {
return;
}
// Let's force landscape mode only if device is in portrait mode and can be held in one hand.
if (matchMedia('(orientation: portrait) and (max-device-width: 768px)').matches) {
screen.orientation.lock('landscape')
<strong>.then(function() {
listenToDeviceOrientationChanges();
})</strong>;
}
}
function listenToDeviceOrientationChanges() {
if (!('DeviceOrientationEvent' in window)) {
return;
}
var previousDeviceOrientation, currentDeviceOrientation;
window.addEventListener(
'deviceorientation',
function onDeviceOrientationChange(event) {
// event.beta represents a front to back motion of the device and
// event.gamma a left to right motion.
if (Math.abs(event.gamma) > 10 || Math.abs(event.beta) < 10) {
previousDeviceOrientation = currentDeviceOrientation;
currentDeviceOrientation = 'landscape';
return;
}
if (Math.abs(event.gamma) < 10 || Math.abs(event.beta) > 10) {
previousDeviceOrientation = currentDeviceOrientation;
// When device is rotated back to portrait, let's unlock screen orientation.
if (previousDeviceOrientation == 'landscape') {
screen.orientation.unlock();
window.removeEventListener(
'deviceorientation',
onDeviceOrientationChange,
);
}
}
},
);
}
보시다시피 원하던 원활한 전체 화면 환경이 구현되었습니다. 실제 동작을 보려면 샘플을 확인하세요.
백그라운드 재생
웹페이지 또는 웹페이지의 동영상이 더 이상 표시되지 않는 것을 감지하면 이를 반영하도록 분석을 업데이트하는 것이 좋습니다. 예를 들어 다른 트랙을 선택하거나 일시중지하거나 사용자에게 맞춤 버튼을 표시하는 등 현재 재생에도 영향을 미칠 수 있습니다.
페이지 공개 상태 변경 시 동영상 일시중지
페이지 가시성 API를 사용하면 페이지의 현재 공개 상태를 확인하고 공개 상태 변경에 관한 알림을 받을 수 있습니다. 아래 코드는 페이지가 숨겨지면 동영상을 일시중지합니다. 이는 화면 잠금이 활성화되어 있거나 탭을 전환할 때 발생합니다.
이제 대부분의 모바일 브라우저에서 일시중지된 동영상을 다시 시작할 수 있는 컨트롤을 브라우저 외부에서 제공하므로 사용자가 백그라운드에서 재생할 수 있는 경우에만 이 동작을 설정하는 것이 좋습니다.
document.addEventListener('visibilitychange', function () {
// Pause video when page is hidden.
if (document.hidden) {
video.pause();
}
});
동영상 공개 상태 변경 시 음소거 버튼 표시/숨기기
새 Intersection Observer API를 사용하면 무료로 더 세부적으로 설정할 수 있습니다. 이 API를 사용하면 관찰된 요소가 브라우저의 표시 영역을 진입하거나 종료할 때 이를 알 수 있습니다.
페이지의 동영상 공개 상태에 따라 음소거 버튼을 표시하거나 숨겨 보겠습니다. 동영상이 재생 중이지만 현재 표시되지 않는 경우 페이지 오른쪽 하단에 미니 음소거 버튼이 표시되어 사용자가 동영상 사운드를 제어할 수 있습니다. volumechange
동영상 이벤트는 음소거 버튼 스타일을 업데이트하는 데 사용됩니다.
<button id="muteButton"></button>
if ('IntersectionObserver' in window) {
// Show/hide mute button based on video visibility in the page.
function onIntersection(entries) {
entries.forEach(function (entry) {
muteButton.hidden = video.paused || entry.isIntersecting;
});
}
var observer = new IntersectionObserver(onIntersection);
observer.observe(video);
}
muteButton.addEventListener('click', function () {
// Mute/unmute video on button click.
video.muted = !video.muted;
});
video.addEventListener('volumechange', function () {
muteButton.classList.toggle('active', video.muted);
});
한 번에 하나의 동영상만 재생
페이지에 동영상이 두 개 이상 있는 경우 사용자가 여러 오디오 트랙을 동시에 들을 필요가 없도록 하나만 재생하고 다른 동영상은 자동으로 일시중지하는 것이 좋습니다.
// This array should be initialized once all videos have been added.
var videos = Array.from(document.querySelectorAll('video'));
videos.forEach(function (video) {
video.addEventListener('play', pauseOtherVideosPlaying);
});
function pauseOtherVideosPlaying(event) {
var videosToPause = videos.filter(function (video) {
return !video.paused && video != event.target;
});
// Pause all other videos currently playing.
videosToPause.forEach(function (video) {
video.pause();
});
}
미디어 알림 맞춤설정
Media Session API를 사용하면 현재 재생 중인 동영상의 메타데이터를 제공하여 미디어 알림을 맞춤설정할 수도 있습니다. 또한 알림이나 미디어 키에서 발생할 수 있는 탐색 또는 트랙 변경과 같은 미디어 관련 이벤트를 처리할 수 있습니다. 실제 동작을 보려면 샘플을 확인하세요.
웹 앱에서 오디오 또는 동영상을 재생하면 알림 트레이에 미디어 알림이 표시됩니다. Android에서 Chrome은 문서 제목과 문서에서 찾을 수 있는 가장 큰 아이콘 이미지를 사용하여 적절한 정보를 표시하기 위해 최선을 다합니다.
Media Session API를 사용하여 제목, 아티스트, 앨범 이름, 아트워크와 같은 일부 미디어 세션 메타데이터를 설정하여 이 미디어 알림을 맞춤설정하는 방법을 살펴보겠습니다.
playPauseButton.addEventListener('click', function(event) {
event.stopPropagation();
if (video.paused) {
video.play()
<strong>.then(function() {
setMediaSession();
});</strong>
} else {
video.pause();
}
});
function setMediaSession() {
if (!('mediaSession' in navigator)) {
return;
}
navigator.mediaSession.metadata = new MediaMetadata({
title: 'Never Gonna Give You Up',
artist: 'Rick Astley',
album: 'Whenever You Need Somebody',
artwork: [
{src: 'https://dummyimage.com/96x96', sizes: '96x96', type: 'image/png'},
{
src: 'https://dummyimage.com/128x128',
sizes: '128x128',
type: 'image/png',
},
{
src: 'https://dummyimage.com/192x192',
sizes: '192x192',
type: 'image/png',
},
{
src: 'https://dummyimage.com/256x256',
sizes: '256x256',
type: 'image/png',
},
{
src: 'https://dummyimage.com/384x384',
sizes: '384x384',
type: 'image/png',
},
{
src: 'https://dummyimage.com/512x512',
sizes: '512x512',
type: 'image/png',
},
],
});
}
재생이 완료되면 알림이 자동으로 사라지므로 미디어 세션을 '해제'할 필요가 없습니다. 재생이 시작되면 현재 navigator.mediaSession.metadata
가 사용됩니다. 따라서 미디어 알림에 항상 관련 정보를 표시하도록 업데이트해야 합니다.
웹 앱에서 재생목록을 제공하는 경우 사용자가 '이전 트랙' 및 '다음 트랙' 아이콘을 사용하여 미디어 알림에서 바로 재생목록을 탐색할 수 있도록 허용할 수 있습니다.
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('previoustrack', function () {
// User clicked "Previous Track" media notification icon.
playPreviousVideo(); // load and play previous video
});
navigator.mediaSession.setActionHandler('nexttrack', function () {
// User clicked "Next Track" media notification icon.
playNextVideo(); // load and play next video
});
}
미디어 작업 핸들러는 유지됩니다. 이는 이벤트를 처리하면 브라우저가 기본 동작을 중단하고 이를 웹 앱이 미디어 작업을 지원한다는 신호로 사용한다는 점을 제외하면 이벤트 리스너 패턴과 매우 유사합니다. 따라서 적절한 작업 핸들러를 설정하지 않으면 미디어 작업 컨트롤이 표시되지 않습니다.
미디어 작업 핸들러를 설정 해제하는 것은 null
에 할당하는 것만큼 간단합니다.
Media Session API를 사용하면 건너뛴 시간을 제어하려는 경우 '뒤로 탐색' 및 '앞으로 탐색' 미디어 알림 아이콘을 표시할 수 있습니다.
if ('mediaSession' in navigator) {
let skipTime = 10; // Time to skip in seconds
navigator.mediaSession.setActionHandler('seekbackward', function () {
// User clicked "Seek Backward" media notification icon.
video.currentTime = Math.max(video.currentTime - skipTime, 0);
});
navigator.mediaSession.setActionHandler('seekforward', function () {
// User clicked "Seek Forward" media notification icon.
video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
});
}
'재생/일시중지' 아이콘은 항상 미디어 알림에 표시되며 관련 이벤트는 브라우저에서 자동으로 처리됩니다. 어떤 이유로든 기본 동작이 작동하지 않는 경우에도 '재생' 및 '일시중지' 미디어 이벤트를 처리할 수 있습니다.
Media Session API의 멋진 점은 알림 목록에 미디어 메타데이터와 컨트롤이 표시되는 유일한 곳이 아니라는 것입니다. 미디어 알림은 페어링된 모든 웨어러블 기기에 자동으로 동기화됩니다. 잠금 화면에도 표시됩니다.