videoJS 워터마크 라이브러리 뿌수기

드엔트론프·2023년 6월 26일
0

들어가며

워터마크에 대한 요구조건이 있었다.
1. videoJS에 워터마크 넣어주세요.
2. 워터마크는 아래 그림처럼 9군데 중 하나를 백오피스에서 설정하면 그 위치에 고정돼야 해요. (원래는 5군데였는데 또 바꿈)
3. 반응형이 되도록 항상 그 위치에 있게 해주세요.(이것도 바뀐 요구사항 ㅎ)

dynamic watermark library

videojs-dynamic-watermark

  • videoJS의 워터마크 라이브러리 중 dynamic watermark 라는 게 있었다.
player.dynamicWatermark({
        elementId: "unique_id",
        watermarkText: `${userId}`,
        changeDuration: 1000000,
        cssText:
          "display: inline-block; color: grey; background-color: transparent; font-size: 1rem; 
						z-index: 9999; position: absolute; 
						@media only screen and (max-width: 992px){ color: red; }", //이게 안됨
      });
  • 사용이 매우 간편해 보이고, 원하는 동작이 잘 반영되는 ‘것처럼’ 보였다.
  • 기본 default option 인 duration을 짧은 시간으로 잡으면, 화면을 동적으로 줄여도 duration에 따라 다시 화면 내 position을 잡는다.

문제

  • 문제는, duration을 매우 길게 가져가면 화면 줄일 때 계속 그 자리에만 있는 것이다. 그래서 화면을 줄이게 되면 혹은 늘리게 되면 자기 자리만 지키고 있어서 잘 동작하지 않는다.
  • CssText라고 default option 에서 수정한 값이 잘 들어가는데, @media 만 동작하지 않는다.

시도

1.라이브러리 까보기

  • 라이브러리를 보니, 랜덤한 위치로 뿌려줄 때 video의 currentHeight, currentWidth 를 쓰는 걸 볼 수 있다.
const videoHeight = player.currentHeight();
const videoWidth = player.currentWidth();
  • player를 찍어보니 나도 currentHeight, currentWidth 를 쓸 수 있고, 그러면 저 값이 동적으로 변할 때마다 dynamicwatermark를 또 찍으면 어떨까 싶었다.
setInterval(() => {
        if (player.currentDimension("width") <= 800) {
          player.dynamicWatermark({
            elementId: "unique_id",
            watermarkText: `${userId}`,
            changeDuration: 1000000, // time interval to change watermark position
            cssText:
              "display: inline-block; color: red; background-color: transparent; font-size: 1rem; z-index: 9999; position: absolute; @media only screen and (max-width: 992px){ color: red; }",
          });
        } else {
          player.dynamicWatermark({
            elementId: "unique_id",
            watermarkText: `${userId}`,
            changeDuration: 1000000, // time interval to change watermark position
            cssText:
              "display: inline-block; color: grey; background-color: transparent; font-size: 1rem; z-index: 9999; position: absolute; @media only screen and (max-width: 992px){ color: red; }",
          });
        }
      }, 5000);
  • setInterval로 5초마다 확인해서 현재 width가 800보다 작으면 color:red 로 해라! 라고 했더니, 기존 워터마크가 그대로 남아있는 문제가 발생했다.
  • 추가로 if문으로 감싸니 'VideoJsPlayer' 형식에 'dynamicWatermark' 속성이 없습니다. 라는 ts 에러는 덤인가보다.

2. div 잡기

  • 이 워터마크 또한 동적으로 생성되는 div이다. 그렇다면, querySelector나 getElementById로 div를 잡아 값을 바꿔주면 어떨까?
  • 아무리 querySelector와 getElementById로 찾아보려해도, null값만 나온다. 왤까 도대체? 분명 player가 ready된 이후 잡으려 하는데도 불구하고, 값이 나오지 않는다.
    • 어쩔수 없이 안되나 싶었는데, setInterval 을 통해 확인할 수 있었다. 처음에는 setTimeout으로 1초 뒤에 잡았더니 됐더라. 그래서 지속적으로 video의 크기를 파악하기 위해 setInterval을 사용. 처음 랜덤 위치에 잡혔다가 설정한 위치로 이동하게 됐다.
  • 다만, interval을 해제하는 clearInterval을 줬음에도 계속 남아있는다. 라이브러리를 까보며 비슷하게 구현했음에도 제대로 동작하지 않아, 수정이 필요한 부분이다.
  • hook 비슷하게 사용할 수 있을 지 테스트 + 정해진 위치에 보여지도록 하게 만들어야 한다. 요구사항이 다시 바뀌었다..

3. 라이브러리를 따라 직접 만들기

  • 사실 오바라고 생각했는데, 하다보니 할 수 있겠다 싶었고, 했다.
  • 다른 방법이 없을까 찾아보다가 문득 하나씩 지우면서 봤는데.. 이미 video 컴포넌트를 기준으로 absolute의 상태다..!
  • 아..!!!! 줄여도 그대로 따라가는구나 ! absolute는 가장 가까운 relative를 따라가는데 정의되어있지 않으면 해당 div의 최상단을 맞춰가니까!
  • 그래서 내가 구현한 코드는 이렇다
import videojs, { VideoJsPlayer } from "video.js";
var window = require("global/window");

function _interopDefaultLegacy(e: any) {
  return e && typeof e === "object" && "default" in e ? e : { default: e };
}

const WatermarkLocationName = {
  TOP_LEFT: "TOP_LEFT",
  TOP_CENTER: "TOP_CENTER",
  TOP_RIGHT: "TOP_RIGHT",
  MID_LEFT: "MID_LEFT",
  MID_CENTER: "MID_CENTER",
  MID_RIGHT: "MID_RIGHT",
  BOTTOM_LEFT: "BOTTOM_LEFT",
  BOTTOM_CENTER: "BOTTOM_CENTER",
  BOTTOM_RIGHT: "BOTTOM_RIGHT",
};

const videojs__default = /*#__PURE__*/ _interopDefaultLegacy(videojs);
const window__default = /*#__PURE__*/ _interopDefaultLegacy(window);
const dom = videojs__default["default"].dom || videojs__default["default"];

const playerCustomWatermark = (
  player: VideoJsPlayer,
  userId: string,
  watermarkLocation: string
) => {
  //워터마크 div 생성
  function createWatermarkElement() {
    const el = dom.createEl(
      "div",
      {},
      {
        id: userId,
      }
    );
    applyCssStyles(el);
    return el;
  }

  //CSS 추가
  function applyCssStyles(el: HTMLDivElement) {
    el.innerHTML = userId;
    el.style.cssText =
      "display: inline-block; color: grey; background-color: transparent; font-size: 1rem; z-index: 9999; position: absolute;";
  }

  //워터마크 위치
  function setWatermarkLocation() {
    let watermarkElement =
      window__default["default"].document.getElementById(userId);
    if (!watermarkElement) {
      watermarkElement = createWatermarkElement();
      player.el().appendChild(watermarkElement);
    }

   switch (true) {
      case watermarkLocation === WatermarkLocationName.TOP_LEFT:
        watermarkElement.style.top = "10%";
        watermarkElement.style.left = "10%";
        break;
      case watermarkLocation === WatermarkLocationName.TOP_CENTER:
        watermarkElement.style.top = "10%";
        watermarkElement.style.left = "50%";
        watermarkElement.style.right = "50%";
        break;
      case watermarkLocation === WatermarkLocationName.TOP_RIGHT:
        // watermarkElement.style.fontSize = "12px";
        watermarkElement.style.top = "10%";
        watermarkElement.style.right = "10%";
        break;
      case watermarkLocation === WatermarkLocationName.MID_LEFT:
        watermarkElement.style.top = "50%";
        watermarkElement.style.left = "10%";
        break;
      case watermarkLocation === WatermarkLocationName.MID_CENTER:
        watermarkElement.style.top = "50%";
        watermarkElement.style.left = "50%";
        watermarkElement.style.right = "50%";
        break;
      case watermarkLocation === WatermarkLocationName.MID_RIGHT:
        watermarkElement.style.top = "50%";
        watermarkElement.style.right = "10%";
        break;
      case watermarkLocation === WatermarkLocationName.BOTTOM_LEFT:
        watermarkElement.style.bottom = "10%";
        watermarkElement.style.left = "10%";
        break;
      case watermarkLocation === WatermarkLocationName.BOTTOM_CENTER:
        watermarkElement.style.bottom = "10%";
        watermarkElement.style.left = "50%";
        watermarkElement.style.right = "50%";
        break;
      case watermarkLocation === WatermarkLocationName.BOTTOM_RIGHT:
        watermarkElement.style.bottom = "10%";
        watermarkElement.style.right = "10%";
        break;
      default:
        break;
    }
  }

  return setWatermarkLocation();
};

export default playerCustomWatermark;
  • switch문을 통해 나름 깔끔하게 적었지만 그래도 중복되는 게 많아 리팩토링이 필요했다.

리팩토링

import videojs, { VideoJsPlayer } from "video.js";
var window = require("global/window");

function _interopDefaultLegacy(e: any) {
  return e && typeof e === "object" && "default" in e ? e : { default: e };
}

const WatermarkLocationName = {
  TOP_LEFT: "TOP_LEFT",
  TOP_CENTER: "TOP_CENTER",
  TOP_RIGHT: "TOP_RIGHT",
  MID_LEFT: "MID_LEFT",
  MID_CENTER: "MID_CENTER",
  MID_RIGHT: "MID_RIGHT",
  BOTTOM_LEFT: "BOTTOM_LEFT",
  BOTTOM_CENTER: "BOTTOM_CENTER",
  BOTTOM_RIGHT: "BOTTOM_RIGHT",
};

const locationStyles = {
  [WatermarkLocationName.TOP_LEFT]: { top: "10%", left: "10%" },
  [WatermarkLocationName.TOP_CENTER]: {
    top: "10%",
    left: "50%",
    right: "50%",
  },
  [WatermarkLocationName.TOP_RIGHT]: { top: "10%", right: "10%" },
  [WatermarkLocationName.MID_LEFT]: { top: "50%", left: "10%" },
  [WatermarkLocationName.MID_CENTER]: {
    top: "50%",
    left: "50%",
    right: "50%",
  },
  [WatermarkLocationName.MID_RIGHT]: { top: "50%", right: "10%" },
  [WatermarkLocationName.BOTTOM_LEFT]: { bottom: "10%", left: "10%" },
  [WatermarkLocationName.BOTTOM_CENTER]: {
    bottom: "10%",
    left: "50%",
    right: "50%",
  },
  [WatermarkLocationName.BOTTOM_RIGHT]: { bottom: "10%", right: "10%" },
};

const videojs__default = /*#__PURE__*/ _interopDefaultLegacy(videojs);
const window__default = /*#__PURE__*/ _interopDefaultLegacy(window);
const dom = videojs__default["default"].dom || videojs__default["default"];

const playerCustomWatermark = (
  player: VideoJsPlayer,
  userId: string,
  watermarkLocation: string
) => {
  //워터마크 div 생성
  function createWatermarkElement() {
    const el = dom.createEl(
      "div",
      {},
      {
        id: userId,
      }
    );
    applyCssStyles(el);
    return el;
  }

  //CSS 추가
  function applyCssStyles(el: HTMLDivElement) {
    el.innerHTML = userId;
    el.style.cssText =
      "display: inline-block; color: grey; background-color: transparent; font-size: 1rem; z-index: 9999; position: absolute;";
  }

  //워터마크 위치
  function setWatermarkLocation() {
    let watermarkElement =
      window__default["default"].document.getElementById(userId);
    if (!watermarkElement) {
      watermarkElement = createWatermarkElement();
      player.el().appendChild(watermarkElement);
    }

    const style = locationStyles[watermarkLocation];
    if (style) {
      Object.assign(watermarkElement.style, style);
    }
    
  }

  return setWatermarkLocation();
};

export default playerCustomWatermark;
  • 여전히 반복적이었지만, 나름대로 줄였다 생각했다.

또 다른 문제 발생

워터마크가 모바일 풀스크린 세로일 때 비디오 안에 들어가지 않는다.

풀스크린

  • 풀스크린을 감지할 수 있을까?
  • 갤럭시는 player.isFullscreen()을 쓰면 풀스크린이라는걸 알 수 있다.
  • IOS는 recoil value인 isFullscreenValue 가 있는데, 기존 코드에서는 불러올 수 가 없다. 왤까?
    • useRecoilValue로 부르고 싶은데, 내가 만든 함수는 hook이 아니고, hook으로 만들어도 기존 VodPlayerWrapper 컴포넌트 안에 있는 함수안에서 정의됐기에 hook의 규칙에 어긋난다.

세로일 때 → VodPlayerWrapper에서 가로 세로를 감지하는 함수를 작성한 후, 변화된 값을 전달한다면?

  • 왜 세로일 때 안들어갈까?
    • 처음 설정한 비율이 비디오div의 N%를 줬으니까
  • 그러면 세로일 때와 가로일 때를 확인해서 세로일 때는 다른 비율을 주면 되지 않을까?
  • 세로일 때와 가로일 때는 어떻게 확인할까 ?
    • window이벤트 중에 ‘resize’가 있다.
    • resize가 되면 뭘 이것저것 하는 게 아니라, resize가 되어 가로, 세로가 바뀌면 바뀐 상태인 것을 알려주자.
    • handleSize를 통해 innerWidth innerHeight의 차이를 통해 가로, 세로임을 알 수 있다.

워터마크가 IOS 풀스크린 세로일 때 비디오 안에 들어가지 않는다.

  • IOS에는 player.isFullscreen()이 동작하지 않는다.
    • 풀스크린 하는 거 자체가 커스텀이었으니까. 가짜 풀스크린이다.
    • 가짜지만) 풀스크린 버튼을 누르면 해당 비디오 div는 분명 변하는데, player.currentHeight는 바로 인지하지 못한다. 물론 resize로 넘겨주려던 innerWidth, innerHeight도.

해결

  • IOS 풀스크린 세로일 때가 문제였다. 이를 해결하기 위해 워터마크 코드를 유심히 살펴보았더니 진짜 문제는 컴포넌트를 싸고 있는 Css의 문제였다..!
  • 거의 이틀 내내 고민했다가 허무하게 해결방법을 찾아내 어이없었지만, 해결방법을 알 수 있어 너무 좋았다.
  • 추가로, 라이브러리를 직접 수정한 이 파일을 훅으로 다시 만들었다.
  • 이유는, css 문제이기 이전에 IOS의 전체화면임을 알아내는 것은 recoil 값인 isFullscreen 인데, recoil을 쓰기 위해선 컴포넌트 최상단에 있어야했다.
  • 라이브러리를 수정한 파일은 useEffect 안에 있었기에 최상단이 아닌 컴포넌트는 recoil값을 사용하면 에러가 났기 때문이다.

최종 코드

import { useState, useEffect, MutableRefObject, useRef } from "react";
import { useRecoilValue } from "recoil";
import videojs, { VideoJsPlayer } from "video.js";
import { fullscreenValue } from "../../../../shared/atoms";

var window = require("global/window");

function _interopDefaultLegacy(e: any) {
  return e && typeof e === "object" && "default" in e ? e : { default: e };
}

const WatermarkLocationName = {
  TOP_LEFT: "TOP_LEFT",
  TOP_CENTER: "TOP_CENTER",
  TOP_RIGHT: "TOP_RIGHT",
  MID_LEFT: "MID_LEFT",
  MID_CENTER: "MID_CENTER",
  MID_RIGHT: "MID_RIGHT",
  BOTTOM_LEFT: "BOTTOM_LEFT",
  BOTTOM_CENTER: "BOTTOM_CENTER",
  BOTTOM_RIGHT: "BOTTOM_RIGHT",
};

//기본 위치 설정
const locationStyles = {
  [WatermarkLocationName.TOP_LEFT]: {
    top: "calc(50% - 30%)",
    left: "calc(20%)",
  },
  [WatermarkLocationName.TOP_CENTER]: {
    top: "calc(50% - 30%)",
    left: "calc(50%)",
  },
  [WatermarkLocationName.TOP_RIGHT]: {
    top: "calc(50% - 30%)",
    left: "calc(50% + 20%)",
  },
  [WatermarkLocationName.MID_LEFT]: {
    top: "calc(50%)",
    left: "calc(20%)",
  },
  [WatermarkLocationName.MID_CENTER]: {
    top: "calc(50%)",
    left: "calc(50%)",
  },
  [WatermarkLocationName.MID_RIGHT]: {
    top: "calc(50%)",
    left: "calc(50% + 30%)",
  },
  [WatermarkLocationName.BOTTOM_LEFT]: {
    top: "calc(50% + 30%)",
    left: "calc(10%)",
  },
  [WatermarkLocationName.BOTTOM_CENTER]: {
    top: "calc(50% + 30%)",
    left: "calc(50%)",
  },
  [WatermarkLocationName.BOTTOM_RIGHT]: {
    top: "calc(50% + 30%)",
    left: "calc(50% + 30%)",
  },
};

const videojs__default = /*#__PURE__*/ _interopDefaultLegacy(videojs);
const window__default = /*#__PURE__*/ _interopDefaultLegacy(window);
const dom = videojs__default["default"].dom || videojs__default["default"];

export const usePlayerCustomWatermark = (
  playerRef: MutableRefObject<VideoJsPlayer | null>,
  userId: string,
  watermarkLocation: string
) => {
  const [watermarkElement, setWatermarkElement] =
    useState<HTMLDivElement | null>(null);
  const isFullscreen = useRecoilValue(fullscreenValue);
  const player = playerRef?.current;

  useEffect(() => {
    if (!player || !userId) {
      return;
    }

    function createWatermarkElement() {
      const el = dom.createEl(
        "div",
        {},
        {
          id: userId,
        }
      );
      applyCssStyles(el);
      return el;
    }

    //CSS 추가
    function applyCssStyles(el: HTMLDivElement) {
      el.innerHTML = userId;
      el.style.cssText =
        "display: inline-block; color: grey; background-color: transparent; font-size: 1rem; z-index: 9999; position: absolute;";
    }

    //워터마크 위치 업데이트
    function updateWatermarkStyle() {
      const watermarkElement =
        window__default["default"].document.getElementById(userId);
      if (!watermarkElement) {
        return;
      }
      const windowWidth = window.innerWidth;
      const windowHeight = window.innerHeight;

      if (player?.isFullscreen() && windowWidth > windowHeight) {
        locationStyles[WatermarkLocationName.TOP_LEFT].top = "calc(50% - 40%)";
        locationStyles[WatermarkLocationName.TOP_CENTER].top =
          "calc(50% - 40%)";
        locationStyles[WatermarkLocationName.TOP_RIGHT].top = "calc(50% - 40%)";
      } else if (player?.isFullscreen() && windowWidth < windowHeight) {
        locationStyles[WatermarkLocationName.TOP_LEFT].top = "calc(50% - 10%)";
        locationStyles[WatermarkLocationName.TOP_CENTER].top =
          "calc(50% - 10%)";
        locationStyles[WatermarkLocationName.TOP_RIGHT].top = "calc(50% - 10%)";
      } else {
        locationStyles[WatermarkLocationName.TOP_LEFT].top = "calc(50% - 40%)";
        locationStyles[WatermarkLocationName.TOP_CENTER].top =
          "calc(50% - 40%)";
        locationStyles[WatermarkLocationName.TOP_RIGHT].top = "calc(50% - 40%)";
      }

      const style = locationStyles[watermarkLocation];
      if (style) {
        Object.assign(watermarkElement.style, style);
      }
    }

    function setWatermarkLocation() {
      if (!watermarkElement) {
        const newWatermarkElement = createWatermarkElement();
        player?.el().appendChild(newWatermarkElement);
        setWatermarkElement(newWatermarkElement);
      }
      updateWatermarkStyle();
    }
    const handleResize = () => {
      updateWatermarkStyle();
    };

    window.addEventListener("resize", handleResize);
    setWatermarkLocation();

    return () => {
      window.removeEventListener("resize", handleResize);
      if (watermarkElement) {
        // player?.el().removeChild(watermarkElement);
      }
    };
  }, [player, userId, watermarkLocation, watermarkElement, isFullscreen]);

  return watermarkElement;
};

배운점

  • 라이브러리를 뜯어보고 직접 훅을 만들었다는 것에 큰 성취감을 느꼈다. 그렇게 어려운 라이브러리가 아니었기에 나름의 짧은 시간에 해결할 수 있었다.
  • 불필요한 중복을 없애고 보기 좋은 코드로 바꿔보려는 노력이 중요하다 생각했다.
  • 문제를 정의할 때 문제를 해결하려 너무 좁게 보지 말고, 넓게 보고 크게 생각해야겠다. 단계별로 나누어 생각하는 습관을 들이자.
profile
왜? 를 깊게 고민하고 해결하는 사람이 되고 싶은 개발자

0개의 댓글