모바일 웹 동영상 재생

프랑수아 보퍼트
프랑수아 보퍼트

웹에서 최고의 모바일 미디어 환경을 만들려면 어떻게 해야 할까요? 아주 쉬워요! 그것은 사용자 참여 그리고 웹페이지의 미디어에 부여하는 중요성에 달려 있습니다. 사용자가 동영상을 보는 이유가 동영상이라면, 사용자의 경험이 몰입도 높고 재참여를 유도해야 한다는 데 모두 동의합니다.

모바일 웹 동영상 재생

이 도움말에서는 미디어 환경을 점진적으로 개선하고 다양한 웹 API를 통해 몰입도 높은 환경을 만드는 방법을 설명합니다. 이러한 이유로 맞춤 컨트롤, 전체 화면, 백그라운드 재생을 갖춘 간단한 모바일 플레이어 환경을 구축하려고 합니다. 이제 샘플을 사용해 보고 GitHub 저장소에서 코드를 찾을 수 있습니다.

맞춤 컨트롤

HTML 레이아웃
그림 1.HTML 레이아웃

미디어 플레이어에 사용할 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
  })`;
});
동영상 메타데이터만
그림 2. 동영상 메타데이터를 보여주는 미디어 플레이어

동영상 재생/일시중지

이제 동영상 메타데이터가 로드되었으므로 사용자가 재생 상태에 따라 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 이벤트 리스너에서 동영상 컨트롤을 조정하는 대신 playpause 동영상 이벤트를 사용합니다. 컨트롤 이벤트를 기반으로 만들면 유연성에 도움이 되며 (나중에 Media Session API에서 설명하겠지만) 브라우저가 재생에 개입하는 경우 컨트롤을 동기화된 상태로 유지할 수 있습니다. 동영상 재생이 시작되면 버튼 상태를 'pause'로 변경하고 동영상 컨트롤을 숨깁니다. 동영상이 일시중지되면 버튼 상태를 '재생'으로 변경하고 동영상 컨트롤을 표시합니다.

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 이벤트 리스너에서 동영상 스타일 지정을 조정하는 대신 실행된 seekingseeked 동영상 이벤트를 사용하여 동영상 밝기를 조정합니다. 내 맞춤 seeking CSS 클래스는 filter: brightness(0);만큼 간단합니다.

video.addEventListener('seeking', function () {
  video.classList.add('seeking');
});

video.addEventListener('seeked', function () {
  video.classList.remove('seeking');
});

지금까지 작성된 항목은 다음과 같습니다. 다음 섹션에서는 전체화면 버튼을 구현합니다.

전체 화면

여기에서는 여러 웹 API를 활용하여 완벽하고 원활한 전체 화면 환경을 만들어 보겠습니다. 실제 과정을 보려면 샘플을 확인하세요.

당연히 모든 기능을 사용할 필요는 없습니다. 자신에게 맞는 흐름을 선택하고 조합하여 맞춤 흐름을 만들면 됩니다

자동 전체 화면 방지

iOS에서 미디어 재생이 시작되면 video 요소가 자동으로 전체 화면 모드로 전환됩니다. 모바일 브라우저에서 미디어 환경을 최대한 맞춤설정하고 제어하려고 하므로 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가 필요합니다. 아직 모든 곳에서 지원되지 않고 현재 일부 브라우저에서는 접두사로 지정되어 있습니다. 따라서 이번이 최초의 점진적 개선이 될 것입니다.

어떤 방식으로 작동하나요? 화면 방향 변경을 감지하는 즉시 브라우저 창이 가로 모드 (즉, 너비가 높이보다 큼)인 경우 전체 화면을 요청합니다. 그렇지 않다면 전체 화면을 종료하겠습니다. 여기까지입니다.

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의 좋은 점은 알림 표시줄이 미디어 메타데이터 및 컨트롤이 표시되는 유일한 위치가 아니라는 점입니다. 미디어 알림은 페어링된 웨어러블 기기에 자동으로 동기화됩니다. 잠금 화면에도 표시됩니다.

의견