🦊

react-youtubeでタイムスタンプを作ってみる

2022/06/18に公開約7,900字

やること

Youtubeの動画の概要やコメントには指定した秒数に飛べるタイムスタンプ機能が用意されています。

このタイムスタンプをYoutube Data APIを使って作る実装をやってみました。

作ったもの

https://cook-clip.vercel.app/
個人的によく観る料理研究家のリュウジさんの動画を使わせていただきました。

実装

使用技術

Next.js, Chakra UI, TypeScript, PlanetScale, Prisma, React Queryを使用しています

動画の埋め込み

まず、react-youtubeを使って動画を埋め込みます。
react-youtubeはYoutube Iframe Player APIをラップしてくれており、動画のidをvideoIdに渡せば簡単にYoutube動画を埋め込むことができます。

https://www.npmjs.com/package/react-youtube
yarn add react-youtube
yarn add -D @types/youtube
src/pages/movie/[id].tsx
const MoviePage: NextPage<Props> = ({ video }) => {
  const [YTPlayer, setYTPlayer] = useState<YT.Player>();
  const opts = {
    width: '100%',
    height: '100%',
  };

  const makeYTPlayer = (e: { target: YT.Player }) => {
    setYTPlayer(e.target);
  };

  return (
      <Box>
        <AspectRatio ratio={16 / 9} maxW='640px' m='0 auto'>
          <YouTube videoId={video.videoId} opts={opts} onReady={makeYTPlayer} />
	</AspectRatio>
      </Box>
  );
};

export default MoviePage;

この時、onReady関数においてYTPlayerというstateをsetしています。
onReady関数は動画の読み込みが完了し、API呼び出しの準備ができると実行されます。
onReadyのeventはtargetというプロパティにYT.playerオブジェクトを持っています。
このplayerオブジェクトにより動画を止めたり、現在の再生時間を取得したりといった操作ができるようになっています。

動画の再生してからの経過時間をDBに登録

src/pages/movie/[id].tsx
const MoviePage: NextPage<Props> = ({ video }) => {
  const { isOpen, onOpen, onClose } = useDisclosure();
  const [YTPlayer, setYTPlayer] = useState<YT.Player>();
  const [startAt, setStartAt] = useState<number>();
  const opts = {
    width: '100%',
    height: '100%',
  };

  const makeYTPlayer = (e: { target: YT.Player }) => {
    setYTPlayer(e.target);
  };

  const handleMakeTimestamp = () => {
    YTPlayer?.pauseVideo();
    setStartAt(YTPlayer?.getCurrentTime());
    onOpen();
  };

  return (
    <Layout>
      <Box textAlign='center' position='fixed' width='100%' pt='50px' bg='white'>
        <AspectRatio ratio={16 / 9} maxW='640px' m='0 auto'>
          <YouTube videoId={video.videoId} opts={opts} onReady={makeYTPlayer} />
        </AspectRatio>
        <Button colorScheme='orange' onClick={handleMakeTimestamp} mt='3' mb='5'>
          タイムスタンプ作成
        </Button>
      </Box>
      <RegistarBookmark
        isOpen={isOpen}
        onClose={onClose}
        startAt={startAt}
        videoId={video.videoId}
      />
    </Layout>
  );
};
src/component/RegisterBookmark.tsx
const RegistarBookmark: React.VFC<Props> = (props) => {
  const { isOpen, onClose, startAt, videoId } = props;

  const schema = z.object({
    title: z.string().min(1, '1文字以上入力してください'),
  });
  type InputType = z.infer<typeof schema>;
  const { register, handleSubmit, formState } = useForm<InputType>({
    resolver: zodResolver(schema),
  });

  const { useHandleRegisterBookmark } = useRegisterBookmark(onClose, startAt, videoId);
  const registerBookmark = useHandleRegisterBookmark();

  return (
    <Modal isOpen={isOpen} onClose={onClose}>
      <ModalOverlay />
      <ModalContent>
        <form
          onSubmit={handleSubmit(async (data) => {
            registerBookmark.mutateAsync(data);
          })}
        >
          <ModalHeader>タイムスタンプ登録</ModalHeader>
          <ModalCloseButton />
          <ModalBody>
            <VStack spacing='2'>
              <Text fontWeight='bold'>タイトル</Text>
              <Input {...register('title')} />
              <Text color='red.500'>{formState.errors.title?.message}</Text>
            </VStack>
          </ModalBody>

          <ModalFooter>
            <Button colorScheme='blue' mr={3} type='submit' isLoading={registerBookmark.isLoading}>
              登録
            </Button>
          </ModalFooter>
        </form>
      </ModalContent>
    </Modal>
  );
};

まず、ボタンを作成し、ボタンを押したとき、動画を停止し、再生からの経過時間を取得します。
この経過時間をタイムスタンプ登録用のモーダルに投げてます

const handleMakeTimestamp = () => {
    YTPlayer?.pauseVideo();
    setStartAt(YTPlayer?.getCurrentTime());
    onOpen();
};

タイムスタンプのタイトルを入力し、登録ボタンを押したときDBに送信します。
省略しますがReact Queryでやっています。

const RegistarBookmark: React.VFC<Props> = (props) => {
  const { isOpen, onClose, startAt, videoId } = props;

  const schema = z.object({
    title: z.string().min(1, '1文字以上入力してください'),
  });
  type InputType = z.infer<typeof schema>;
  const { register, handleSubmit, formState } = useForm<InputType>({
    resolver: zodResolver(schema),
  });

  const { useHandleRegisterBookmark } = useRegisterBookmark(onClose, startAt, videoId);
  const registerBookmark = useHandleRegisterBookmark();

  return (
    <Modal isOpen={isOpen} onClose={onClose}>
      <ModalOverlay />
      <ModalContent>
        <form
          onSubmit={handleSubmit(async (data) => {
            registerBookmark.mutateAsync(data);
          })}
        >
          <ModalHeader>タイムスタンプ登録</ModalHeader>
          <ModalCloseButton />
          <ModalBody>
            <VStack spacing='2'>
              <Text fontWeight='bold'>タイトル</Text>
              <Input {...register('title')} />
              <Text color='red.500'>{formState.errors.title?.message}</Text>
            </VStack>
          </ModalBody>

          <ModalFooter>
            <Button colorScheme='blue' mr={3} type='submit' isLoading={registerBookmark.isLoading}>
              登録
            </Button>
          </ModalFooter>
        </form>
      </ModalContent>
    </Modal>
  );
};

登録したタイムスタンプを表示

タイムスタンプ表示用のコンポーネントです。

const BookmarkOfVideo: React.VFC<Props> = (props) => {
  const { bookmark, ytPlayer, videoId } = props;
  const toHHMMSS = (secValue: Decimal) => {
    const secInt = parseInt(secValue.toString(), 10);
    const hours = Math.floor(secInt / 3600);
    const minutes = Math.floor((secInt - hours * 3600) / 60);
    const seconds = secInt - hours * 3600 - minutes * 60;

    const formattedHours = hours < 10 ? '0' + hours : hours;
    const formattedMinutes = minutes < 10 ? '0' + minutes : minutes;
    const formattedSecounds = seconds < 10 ? '0' + seconds : seconds;

    if (formattedHours == '00') {
      return formattedMinutes + ':' + formattedSecounds;
    }

    return formattedHours + ':' + formattedMinutes + ':' + formattedSecounds;
  };

  const moveToTimestamp = () => {
    ytPlayer?.seekTo(parseInt(bookmark.startAt.toString(), 10), true);
  };

  return (
    <>
      <Flex>
        <Text fontSize='x-large'><a href='#' onClick={moveToTimestamp} className='link' style={{ color: '#639bb7' }}>
            {toHHMMSS(bookmark.startAt)} : {bookmark.title}
          </a>
        </Text>
      </Flex>
    </>
  );
};

onClick時にseekToによりタイムスタンプの時間まで移動できるようになっています。

const moveToTimestamp = () => {
    ytPlayer?.seekTo(parseInt(bookmark.startAt.toString(), 10), true);
};

これを動画ページに埋め込みます

src/pages/movie/[id].tsx
const MoviePage: NextPage<Props> = ({ video }) => {
  ~~省略~~
  const { isLoading, isError, data } = useFetchBookmarksOfVideo(
    video?.videoId
  );

  return (
    <Layout>
      <Box>
        <AspectRatio ratio={16 / 9} maxW='640px' m='0 auto'>
          <YouTube videoId={video.videoId} opts={opts} onReady={makeYTPlayer} />
        </AspectRatio>
        <Button colorScheme='orange' onClick={handleMakeTimestamp} mt='3' mb='5'>
          タイムスタンプ作成
        </Button>
      </Box>
      <Box>
        <Container>
          {data?.map((item) => (
            <BookmarkOfVideo
              bookmark={item}
              ytPlayer={YTPlayer}
              key={item.id}
              videoId={video.videoId}
            />
          ))}
        </Container>
      </Box>
        ~~省略~~
    </Layout>
  );
};

最後に

react-youtubeによるタイムスタンプの作成を実装しました。
ちなみに動画情報はYoutube Data APIを使ってDBに登録しています。

ソースコード

https://github.com/RyoMasumura1201/cook-clip

Discussion

ログインするとコメントできます