티스토리 뷰
지난 포스팅 : [WillBe 화상 면접 커뮤니티] React Player를 통한 custom player만들기 (1)
- 재생, 멈춤
- 3초 후, 3초전
- 소리 조절 + 음소거
- 배속 (0.5배속, 1배속, 1.5배속, 2배속)
- 전체화면 (fullscreen)
오늘 다룰 내용
6. 재생시간 툴팁으로 표시
7-1. 좋아요, 스크랩 동영상 내에서 처리
7-2. 하이라이트 Top 3 시간 누르면 해당 시간으로 이동
+ TroubleShooting
6. 재생시간 툴팁으로 표시
이미지와 같이 재생바와 표시되는 시간 툴팁을 구현하는 부분이다.
필요한 메소드는 재생 컨트롤러를 인식하는 seekTo(), 현재시간을 초로 반환하는 getCurrnetTime(), 총 시간을 반환하는 getDuration() 로 이 세가지가 필요하다.
Method | Description |
seekTo(amount, type) | Seek to the given number of seconds, or fraction if amount is between 0 and 1 ◦ type parameter lets you specify 'seconds' or 'fraction' to override default behaviour |
getCurrentTime() | Returns the number of seconds that have been played ◦ Returns null if unavailable |
getDuration() | Returns the duration (in seconds) of the currently playing media ◦ Returns null if duration is unavailable |
format 함수
기본적으로 초로 반환되는 시간을 00:00 형식으로 바꾸어주는 format함수를 만들었다.
function format(seconds) {
if (isNaN(seconds)) {
return `00:00`;
}
const date = new Date(seconds * 1000);
const hh = date.getUTCHours();
const mm = date.getUTCMinutes();
const ss = pad(date.getUTCSeconds());
if (hh) {
return `${hh}:${pad(mm)}:${ss}`;
}
return `${mm}:${ss}`;
}
function pad(string) {
return ("0" + string).slice(-2);
}
함수
timeDisplayFormat으로 state를 관리해주고 있는 이유는 아래 예시처럼 남은시간버전과 현재시간 버전 둘다 구현하기 위해서이다.
"normal"이면 현재시점, "remaining"이면 남은 시간을 보여주며 시간이 표시되는 div를 클릭시 함수를 발생시킨다.
const Video = (props) => {
// 기본 format 상태를 "nomal"로 설정
const [timeDisplayFormat, setTimeDisplayFormat] = useState("normal");
// 재생 컨트롤러가 onChange()시 발생하는 함수
const onSeekChangeHandler = (e, newValue) => {
setState({ ...state, played: parseFloat(newValue / 100) });
};
// 재생 컨트롤러를 움직이고 있을 때 발생하는 함수
const seekMouseDownHandler = (e) => {
setState({ ...state, seeking: true });
};
// 재생 컨트롤러에서 조정을 완료했을 때 (slider onChangeCommitted시 발생하는 함수)
const seekMouseUpHandler = (e, newValue) => {
setState({ ...state, seeking: false });
videoRef.current.seekTo(newValue / 100, "fraction");
};
// 재생 시간을 재생되는 시간 or 남은시간으로 바꾸어주는 함수
const displayFormatHandler = () => {
setTimeDisplayFormat(
// 처음상태인 nomal이면 렌더링 됐을 때 "remaining"으로 바꾸어준다.
timeDisplayFormat === "normal" ? "remaining" : "normal"
);
};
// 현재 재생 시점
const currentTime =
videoRef && videoRef.current ? videoRef.current.getCurrentTime() : "00:00";
// 영상 총 시간
const duration =
videoRef && videoRef.current ? videoRef.current.getDuration() : "00:00";
// 남은시간
const elapsedTime =
// "nomal"일시에는 현재 시간, 아니라면("remaining"이면) 총 시간에서 현재시간을 빼준다.
timeDisplayFormat === "normal"
? format(currentTime)
: `-${format(duration - currentTime)}`;
// 영상 총 시간을 00:00 형식으로 바꾼다. (영상 하단 00:00/00:00 에 들어갈 부분)
const totalDuration = format(duration);
return (
...
);
};
export default Video;
Trouble shotting
tooltip기능은 slider 에서 제공하기는 한다. valueLabelDisplay="auto"로 해주면 자동으로 생긴다.
하지만 골머리를 썩혔던 것이 우리가 보는 00:00시간 형태로 보여지기 위해서는 value에 내가 format해준 elapsedTime을 넣어줬어야 했는데 그 부분이 잘 동작하지 않았다.
기본적으로 value를 number만 취급했기 때문이었고 00:00의 string type은 mui에서 인식하지 못했다.
방법을 찾으려 구글링을 하던 중 mui API문서에서 valueLabelFormat() 라는 함수를 찾았고 이 덕분에 value를 string으로 변환할 수 있었다.
valueLabelFormat | func | string |
(x) => x |
The format function the value label's value.
When a function is provided, it should have the following signature: - {number} value The value label's value to format - {number} index The value label's index to format |
* valueLabelFormat()을 적용한 함수
const valueLabelFormat = (value) => {
let _elapsedTime = value;
_elapsedTime = elapsedTime;
return _elapsedTime;
};
적용코드
// video controller
<div className="slider">
<PrettoSlider
valueLabelDisplay="auto"
min={0}
max={100}
value={played * 100}
onChange={onSeek}
onMouseDown={onSeekMouseDown}
onChangeCommitted={onSeekMouseUp}
valueLabelFormat={valueLabelFormat}
/>
</div>;
// video display time
<button className="video_time" onClick={onChangeDisplayFormat}>
{elapsedTime}/{totalDuration}
</button>;
7-1. 좋아요, 스크랩 동영상 내에서 처리
무제한으로 좋아요를 할 수 있는 기능은 추후에 추가된 기능이다. 기존의 스크랩기능이 있었기 때문에 좋아요가 있어도 의미가 없었고, 영상과 직접적으로 반응할 수 있는 기능이 어떤게 있을까 생각하다 나온 아이디어이다.
유투브에서 댓글로 시간을 달면 해당 시간으로 가는 것과, 인스타그램 라이브에 있는 무제한으로 좋아요를 할 수 있는 기능을 접목하여
면접영상이 재생되는 동안 유저들의 좋아요를 구간별로 산정해 서버에 요청하면 서버에서 좋아요를 가장 많이 받은 Top3구간을 내려주는 방식으로 구현하였다.
클라이언트쪽에서 interview ID, 해당 시점의 시간 time, 그 구간동안 받은 좋아요 개수 count를 요청하면 TOP3를 내려주고, TOP3가 없다면 음수를 반환해준다.
실시간으로 이뤄지는 것은 아니지만 실시간 느낌을 주려고 호출할 때마다 오는 responsive의 좋아요 TOP3 구간을 바로 반영시켰다.
기능 | URL | Method | Request | Responsive |
좋아요 | /api/likes | POST |
{
"headers": {
"Authorization": "Bearer ${ token }",
}
”data”: {
”InterviewId”:
”time”:
”count”:
}
}
|
“data”:{
"likesData":{
"0":1,
"12":1
},
"topOne":0,
"toptwo":12,,
"topThree":-7, // 좋아요가 없으면 반환
”totalCount:30,
}
|
좋아요 클릭시 발생하는 함수와 POST 요청
Trouble shotting
비디오 구간별 좋아요 구현시 일정시간마다 setInterval() 을 사용하여 api를 호출하는데, 이 때 좋아요 개수(likeCount)를 useState() hook을 사용해 보내주었다. 하지만 실제로 실행했을 때 좋아요 클릭을 멈춰야만 api가 호출이 되는 이슈가 있었다.
이는 useState도 비동기 작업이라 좋아요를 누르는 동안 api호출이 되지 않고 있었던 이슈였다. 해당 이슈는 전역변수로 likeCount를 선언해 값을 측정하고 보내는 방식으로 해결하였다.
// 하트 아이콘 클릭시 발생하는 함수 likeCount는 컴포넌트 밖에서 변수로 선언하였다.
const addLikeHandler = () => {
if (!token) {
alert("로그인이 필요한 기능입니다. ");
return;
}
likeCount++;
setLikes((prev) => ({
likeTime: [...prev.likeTime],
like: [...prev.like, new Date().getTime()],
}));
};
useEffect(() => {
// 일정 시간마다 실행할 수 있도록 setInterval()를 사용하였다.
const intervalPost = setInterval(async () => {
// 해당 구간동안 좋아요 클릭 이벤트가 없었다면 API요청을 하지 않고 return시킨다.
if (likeCount === 0) {
return;
}
// interval이 돌아 함수가 실행되는 시점의 시간
let currentTime = Math.floor(videoRef.current.getCurrentTime());
const likeData = {
interviewId: cardId,
time: currentTime,
count: likeCount,
};
try {
// 좋아요 API 호출
const { data } = await highlightApis.addHighlight(likeData);
// 서버에서 좋아요가 없으면 음수를 내려주므로 filter로 해당 부분은 거른다.
const filteredLike = [data.topOne, data.topTwo, data.topThree]
.filter((time) => time >= 0)
// filter된 like배열을 새 배열을 선언해 time과
// format()함수를 통해 실제로 보여질 시간인 displaytime도 넣어준다.
const newLike = [];
filteredLike.map((time) =>
newLike.push({
time,
display: format(time),
})
);
setLikes((prev) => ({
likeTime: newLike,
like: [...prev.like, new Date().getTime()],
}));
likeCount = 0;
} catch (err) {
Sentry.captureException(`Add highlight : ${err}`);
}
}, 6000);
return () => clearInterval(intervalPost);
}, [cardId, token]);
좋아요 애니메이션 및 툴팁
Vimeo에서 좋아요나 스크랩을 할 때 나타나는 툴팁이 좋아보여서 나도 툴팁으로 안내를 했다. 툴팁은 css로 표현했다.
애니메이션은 Bubble.jsx에서 확인할 수 있다.
<div>
<button
className="like_btn"
onClick={token ? addLikeHandler : openModalHandler}
>
<div className="tooltip">좋았던 순간을 클릭하세요!</div>
<LikeIcon size={35} />
</button>
{likes.like.map((id) => (
<Bubble onAnimationEnd={cleanLike.current} key={id} id={id} />
))}
</div>;
7-2. 하이라이트 Top 3 시간 누르면 해당 시간으로 이동
Highlight bar
구간별 좋아요 개수에 따라 하이라이트 1위부터 3위까지 순차적으로 반영이되고 해당 시간을 클릭하면 그 시점에서 재생된다.
하이라이트 바를 구현하기 위해 서버로부터 좋아요 TOP3 데이터를 받는다.
useEffect(() => {
const getHighlight = async () => {
const { data } = await highlightApis.getHighlight(cardId);
const filteredLike = [data.topOne, data.topTwo, data.topThree]
.filter((time) => time >= 0)
.map((item) => (item === 0 ? 3 : item));
const newLike = [];
filteredLike.map((time) =>
newLike.push({
time,
display: format(time),
})
);
setLikes((prev) => ({
likeTime: newLike,
like: [...prev.like, new Date().getTime()],
}));
};
try {
getHighlight();
} catch (err) {
Sentry.captureException(`Get highlight : ${err}`);
}
}, [cardId]);
적용 코드
필요한 설명은 주석으로 달았다.
<HightLight>
<div className="highlight_bar">
<div className="contents_box">
<div className="title">
<span>
하이라이트 <img src={questionMark} alt="questionMark" />
// 하이라이트 tooltip
<div className="tooltip_highlight">
가장 좋아요를 많이 받은 TOP3 시간입니다. <br /> 클릭하면 해당
시간대로 이동합니다.
</div>
</span>
<span className="line">|</span>
</div>
// 하이라이트 데이터가 없을시 보여주는 안내문구
{likes?.likeTime.length === 0 ? (
<div className="noti">
아직 하이라이트 시간이 없습니다. 좋아요를 눌러 하이라이트를
채워주세요!
</div>
) : (
// TOP3시간을 담은 배열 likeTime
likes.likeTime.map((like, index) => {
return (
<div
className="timestamp_box"
key={index}
onClick={() => {
// Top3 시간 클릭시 영상이 해당 시간으로 이동시킨다.
videoRef.current.seekTo(like.time);
// 해당 시간대로 갈 때 control bar를 보여줬다가 1초후에 자동으로 사라지게 한다.
controlsRef.current.style.visibility = "visible";
setTimeout(() => {
controlsRef.current.style.visibility = "hidden";
}, 1000);
}}
elevation={3}
>
<button>{like.display}</button>
</div>
);
})
)}
</div>
</div>
</HightLight>
짧은 회고
Reference
'항해99 > 실전프로젝트' 카테고리의 다른 글
[WillBe 화상 면접 커뮤니티] React Player를 통한 custom player만들기 (1) (0) | 2022.05.23 |
---|---|
[WillBe 화상 면접 커뮤니티] S3 Pre-signed URL 를 통한 비디오 주고받기 (0) | 2022.05.20 |
[WillBe 화상 면접 커뮤니티] WebRTC, codec 조사 및 적용 (+Trouble Shooting) (0) | 2022.05.18 |
[WillBe 화상 면접 커뮤니티] 프로젝트 주제, 컨셉, 기술챌린지 소개 (0) | 2022.05.15 |
- Total
- Today
- Yesterday
- 실전프로젝트
- React
- GIT
- 무한스크롤
- 리액트
- 알고리즘자바스크립트
- 프로그래머스 자바스크립트
- 프로그래머스
- css
- 자바스크립트 비동기 처리
- network
- 항해99
- 자바스크립트알고리즘
- 백준
- React Query
- 자바스크립트
- cs50
- reactquery
- 모두를위한컴퓨터과학
- javascript
- 프로그래머스 베스트앨범 자바스크립트
- 모두를 위한 컴퓨터 과학
- 리액트네이티브
- 네트워크
- 자바스크립트 클로저
- github
- python
- 타입스크립트
- 클로저
- html
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |