웹에서 최고의 모바일 미디어 환경을 만들려면 어떻게 해야 할까요? 참 쉽죠? 이는 모두 사용자 참여도와 웹페이지에서 미디어에 부여하는 중요도에 따라 달라집니다. 사용자가 방문하는 이유가 동영상이라면 사용자 경험은 몰입도가 높고 재참여를 유도해야 한다는 점에 모두 동의하실 것입니다.
이 도움말에서는 다양한 웹 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');
});
timeupdate 동영상 이벤트를 통해 동영상 currentTime 속성으로 표시된 시간이 변경되면 맞춤 컨트롤이 표시되는 경우 맞춤 컨트롤도 업데이트합니다.
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 요소가 자동으로 전체 화면 모드로 전환됩니다. 모바일 브라우저에서 미디어 환경을 최대한 맞춤설정하고 제어하려고 하므로, iPhone에서 인라인으로 재생하고 재생이 시작될 때 전체 화면 모드로 전환되지 않도록 video 요소의 playsinline 속성을 설정하는 것이 좋습니다. 이는 다른 브라우저에 부작용을 일으키지 않습니다.
<div id="videoContainer"></div>
<video id="video" src="file.mp4"></video><strong>playsinline</strong></video>
<div id="videoControls">...</div>
</div>
버튼 클릭 시 전체 화면 전환
자동 전체 화면을 방지하므로 전체 화면 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);
});
화면 방향 변경 시 전체 화면 전환
사용자가 기기를 가로 모드로 회전하면 이를 스마트하게 처리하여 자동으로 전체 화면을 요청해 몰입도 높은 환경을 만들어 보겠습니다. 이를 위해서는 화면 방향 API가 필요하지만, 이 API는 아직 모든 곳에서 지원되지 않으며 당시 일부 브라우저에서는 여전히 접두사가 붙어 있었습니다. 따라서 이것이 첫 번째 점진적 개선이 될 것입니다.
작동 방식 화면 방향 변경이 감지되는 즉시 브라우저 창이 가로 모드인 경우 (즉, 너비가 높이보다 큰 경우) 전체 화면을 요청합니다. 그렇지 않다면 전체 화면을 종료합니다. 여기까지입니다.
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,
);
}
}
},
);
}
보시다시피, 이 환경은 우리가 찾고 있던 원활한 전체 화면 환경입니다. 실제 작동 모습을 보려면 샘플을 확인하세요.
백그라운드 재생
웹페이지 또는 웹페이지의 동영상이 더 이상 표시되지 않는 것을 감지하면 이를 반영하도록 분석을 업데이트할 수 있습니다. 이는 다른 트랙을 선택하거나 일시중지하거나 사용자에게 맞춤 버튼을 표시하는 등 현재 재생에도 영향을 미칠 수 있습니다.
페이지 공개 상태 변경 시 동영상 일시중지
Page Visibility 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의 장점은 미디어 메타데이터와 컨트롤이 알림 트레이에만 표시되는 것이 아니라는 점입니다. 미디어 알림은 페어링된 웨어러블 기기에 자동으로 동기화됩니다. 잠금 화면에도 표시됩니다.