티스토리 뷰

반응형

 

 

지난 포스팅 : [WillBe 화상 면접 커뮤니티] React Player를 통한 custom player만들기 (1)

  1. 재생, 멈춤
  2. 3초 후, 3초전 
  3. 소리 조절 + 음소거
  4. 배속 (0.5배속, 1배속, 1.5배속, 2배속) 
  5. 전체화면 (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

 

반응형