유튜브 자동재생 웹사이트에서 구현하기
영상 자동재생 구현하기
사용자들이 영상에 흥미를 가지게 하기 위해서 자동재생 기능을 구현하게 되었습니다. 유튜브의 작동방식을 최대한 분석하면서 이를 기반하여 구현하였습니다.
유튜브를 분석한 결과 자동재생 기능은 크게 두 가지로 나누어집니다.
- PC에서 영상 자동재생
- 스마트폰에서 영상 자동재생
PC에서 영상 자동재생
웹에서 영상을 재생하는 방법은 마우스 커서를 이용합니다.
마우스 커서를 위에 올려놓으면 영상을 재생하고 벗어나면 재생을 멈춥니다.
하지만 구현하기 위해서는 더 디테일한 요소를 캐치해야 합니다.
1. 썸네일이 존재하고 마우스 커서를 올려놓으면 썸네일이 사라지면서 영상이 재생됩니다.
좀더 관찰하다보면 마우스 커서를 올려놓는다고 바로 재생이 되지 않습니다.
그냥 마우스가 움직일때 수시로 재생되는 것을 막기위해서인지 올려놓고 0.2초쯤 뒤에 재생되는것을 관찰할 수 있습니다.
마우스 커서가 올라가는 이벤트에 디바운싱을 적용했다고 유추할 수 있습니다.
2. 영상은 즉시 재생되며 그전에 재생된 지점을 이어서 재생합니다.
마우스 커서를 올려놓는다고 그때 영상 플레이어를 로드하는게 아니라 미리 로드해놓고 있습니다.
또한 벗어나고 다시 올려놓았을때 전 재생지점을 이어서 재생합니다.
코드로는 비교적 쉽게 구현할 수 있습니다.
<VideoCard
onMouseEnter={() => {
playVideo();
}}
onMouseLeave={() => {
stopVideo();
}}
>
{isPlaying ? null : <ThumbnailImage />}
<ReactPlayer />
</VideoCard>
<VideoCard
onMouseEnter={() => {
playVideo();
}}
onMouseLeave={() => {
stopVideo();
}}
>
{isPlaying ? null : <ThumbnailImage />}
<ReactPlayer />
</VideoCard>
onMouseEnter
와 onMouseLeave
를 이용하여 마우스가 올라가 있는지를 파악하고 썸네일을 사라지게 하거나 보여줍니다.
실제 코드는 위보다 좀더 복잡합니다. 자동재생 기능을 켰는지 껐는지를 고려해야하고 영상이 재생가능한지 등을 추가로 고려하여 최종적으로 코드를 완성합니다.
스마트폰에서 영상 자동재생
스마트폰에서 자동재생이 동작하는건 더 복잡합니다.
PC처럼 커서가 있는게 아니기에 포커싱의 개념을 정의해야합니다.
PC에서는 마우스 커서의 호버이벤트가 포커싱을 나타내었습니다.
스마트폰에서는 커서가 없기 때문에 새로운 포커싱 기준이 필요합니다. 그것을 유튜브에서는 영상 전체가 화면안에 들어와 있는 것으로 정의한것 같습니다.
그런데 이렇게만 정의를 하게되면 한가지 문제가 생깁니다.
화면이 긴 스마트폰의 경우는? 한 화면에 두 개의 온전한 영상이 들어와있을수 있습니다. 이때 두 개의 영상이 동시에 재생되어서는 안됩니다.
따라서 온전히 들어와 있는 영상중에 상단에 있는것으로 구현 아이디어를 정했습니다.
const handleScroll = () => {
if (!cardRef.current) return;
const rect = cardRef.current.getBoundingClientRect();
const isTopPosition =
rect.top < standardPosition && rect.bottom > standardPosition;
setIsPlaying(isTopPosition);
};
const debouncedHandleScroll = debouncing(handleScroll, 100);
useEffect(() => {
window.addEventListener('scroll', debouncedHandleScroll);
return () => {
window.removeEventListener('scroll', debouncedHandleScroll);
};
}, [debouncedHandleScroll]);
const handleScroll = () => {
if (!cardRef.current) return;
const rect = cardRef.current.getBoundingClientRect();
const isTopPosition =
rect.top < standardPosition && rect.bottom > standardPosition;
setIsPlaying(isTopPosition);
};
const debouncedHandleScroll = debouncing(handleScroll, 100);
useEffect(() => {
window.addEventListener('scroll', debouncedHandleScroll);
return () => {
window.removeEventListener('scroll', debouncedHandleScroll);
};
}, [debouncedHandleScroll]);
코드에서는 위와같이 스크롤 이벤트를 감지해서 카드의 위치를 기준으로 재생을 하도록 구성했습니다.
위 사이트는 간단히 다시 구현해본 결과이며 특정 위치라는 것을 선으로 표시하였습니다.
선 위치에 카드가 오게 되면 재생됩니다.
사실 이 기능을 추가할때의 유튜브 재생 정책은 '재생가능한 영상중 상단에 있는 것'이 아니었습니다. 재생중인것이 하단에 내려가더라도 화면에서 아웃되지 않으면 계속 재생이 유지되었습니다.
최근에는 바뀌었는데, 재생이 끊기지 않게하는 것에서 (항상 상단의 영상이 재생되어) 시선의 이동을 줄여주는 방향으로 수정된 것 같습니다.
그렇게 1차적으로 구현한 기능에는 문제점이 있었습니다
-
재생이 되는 카드의 위치를 어떻게 정할것인가? - 기준모호
-
다양한 페이지와, 다양한 영상카드가 나타나더라도 계속 사용가능한 지속가능한 구현방법인가? - 그때마다 다르게 해야함, 변수가 많음
-
한 화면에 재생 불가능한 영상이 상단에 있고 재생이 가능한 영상이 하단에 있을 때 재생되지 않는 문제 - 근본적인 문제
비록 지속가능한 구현방법은 아닐지라도 동작에는 문제가 없어보였지만 위와 같은 제한사항에 의해 동작에도 문제가 있다는 것을 알게되었습니다.
특히 마지막 문제가 치명적이었는데, 이때 옳게된 동작은 '아래에 위치하는 재생가능한 영상이 재생되는 것'입니다.
아예 구현방법을 바꿔야 했습니다
"상단"보다도 "화면 안"에 집중하였습니다.- 영상이 화면안에 들어오게 되면, 영상을 '리스트'에 추가합니다. 이때 재생이 불가능한 경우는 추가하지 않습니다.
- 재생가능한 영상 리스트중 가장 상단의 있는 영상을 재생합니다.
- 화면 밖으로 나가게 되면 리스트에서 제거합니다.
useEffect(() => {
let currentCardRef = cardRef.current;
const observer = new IntersectionObserver(
(entries) => {
const isPlayable = videoData.url !== '';
if (entries[0].isIntersecting && isPlayable) {
addActiveVideoIndexList();
} else {
removeActiveVideoIndexList();
}
},
{ threshold: 0.7 },
);
if (cardRef.current) {
currentCardRef = cardRef.current;
observer.observe(currentCardRef);
}
return () => {
if (currentCardRef) observer.unobserve(currentCardRef);
};
}, [addActiveVideoIndexList, removeActiveVideoIndexList, videoData.url]);
useEffect(() => {
let currentCardRef = cardRef.current;
const observer = new IntersectionObserver(
(entries) => {
const isPlayable = videoData.url !== '';
if (entries[0].isIntersecting && isPlayable) {
addActiveVideoIndexList();
} else {
removeActiveVideoIndexList();
}
},
{ threshold: 0.7 },
);
if (cardRef.current) {
currentCardRef = cardRef.current;
observer.observe(currentCardRef);
}
return () => {
if (currentCardRef) observer.unobserve(currentCardRef);
};
}, [addActiveVideoIndexList, removeActiveVideoIndexList, videoData.url]);
영상이 화면에 들어오고 나가는 것은 IntersectionObserver를 이용하여 감지해줍니다.
영상이 재생가능한 경우에만 리스트에 추가해줍니다.
실제 로직에서는 유튜브 api를 사용해서 공유를 비허용한 경우, 삭제된 영상 등 다양한 경우를 고려하였습니다.
그리고 리스트에서 index가 가장 작은 영상을 재생합니다. 이를 통해 하단에만 영상이 있는 경우를 고려했습니다.
이때 threshold 속성을 통해서 70%만 보여도 리스트에 추가하도록 설정하였습니다.
추가적인 고민
이 기능에 대해서 좀 더 고민을 했습니다.
사용자에게 새로운 경험을 주기 위해서 추가했지만 의도치 않게 많은 데이터를 쓰게 만들수도 있었습니다.
그래서 설정페이지에 자동재생 on/off 기능을 추가했고 기본설정인 off인 상태에서는 영상 플레이어를 로드하지 않게 처리를 하여 최종 완성하였습니다.