250312 TIL #624 Video.JS를 이용한 Floating Video 구현

김춘복·2025년 3월 12일
0

TIL : Today I Learned

목록 보기
628/636

Today I Learned

video.js를 가지고 Floating window Video를 구현 시도 해봤다.
node.js를 서버로 두고 프론트 페이지만 프로토타입으로 빠르게 구현해봤다.


Video.JS

HTML5 기반의 오픈소스 비디오 플레이어 라이브러리.
웹에서 쉽게 재생할 수 있도록 도와주는 JavaScript 라이브러리.

설치

  • npm 설치
npm install video.js
  • cdn 설치
<link href="https://vjs.zencdn.net/8.3.0/video-js.css" rel="stylesheet" />
<script src="https://vjs.zencdn.net/8.3.0/video.min.js"></script>

주요 구현 기능

  • z-index를 이용한 floating window css로 구현
  • drag-handler를 구현해 floating window 이동
  • PiP 사용 시 floating window 창 안보임
  • videojs-hls-quality-selector 플러그인을 이용한 화질 선택
  • @theonlyducks/videojs-zoom 플러그인을 이용한 확대
  • 다중 자막
  • 배속 선택
  • 10초 앞, 10초 뒤 이동 버튼
  • 컨트롤러
  • 볼륨 세로
  • 페이지 나가면 현재 재생 위치 기억해서 다음 접속 때 이어서 재생

index.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Floating Window Player Test</title>
    <link rel="stylesheet" href="style.css">
    <link href="//vjs.zencdn.net/8.3.0/video-js.min.css" rel="stylesheet">
    <link rel="stylesheet" type="text/css" href="../node_modules/@theonlyducks/videojs-zoom//dist/videojs-zoom.css">
    <link rel="shortcut icon" href="#">
</head>
<body>
    <h1>video test</h1>

    <script src="script.js"></script>
    <div class="vertical-split-container">
        <div class="left-panel">
            <h1>Left 학습 내용</h1>
            <p>오늘의 내용</p>
            <div id="floating-video-container" class="floating-video-container">
                <!-- Video.js player -->
                <video-js
                    id="floating-video"
                    class="video-js vjs-default-skin"
                    width="600"
                    height="338"
                >   

                    <!-- <source src="https://customer-f33zs165nr7gyfy4.cloudflarestream.com/6b9e68b07dfee8cc2d116e4c51d6a957/manifest/video.m3u8" type="application/x-mpegURL"> -->
                    <!-- <source src="video/quality/master.m3u8" type="application/x-mpegURL"> -->
                    <!-- <source src="//vjs.zencdn.net/v/oceans.mp4" type="video/mp4"></source> -->
                    
                    <track kind="captions" src="subs/captions.en.vtt" srclang="en" label="English" default>
                    <track kind="captions" src="subs/captions.ru.vtt" srclang="ru" label="Russian">
                    
                </video-js>
                <div class="drag-handle"></div>
            </div>
            <img src="cat.jpg" alt="cat image" class="cat-image">
            <br>
            <textarea></textarea>
        </div>
        <iframe src="right-panel.html"></iframe>
    </div>

    <script src="script.js"></script>
    <script src="https://vjs.zencdn.net/8.3.0/video.min.js"></script>
    <script src="../node_modules/videojs-hls-quality-selector/dist/videojs-hls-quality-selector.min.js"></script>
    <script src="../node_modules/@theonlyducks/videojs-zoom/dist/videojs-zoom.js"></script>
    
</body>
</html>

script.js

document.addEventListener('DOMContentLoaded', function () {
    const videoContainer = document.getElementById('floating-video-container');
    const dragHandle = document.querySelector('.drag-handle');
    
    // video js 초기화
    const player = videojs('floating-video', {
        controls: true,
        autoplay: false,
        preload: 'auto',
        enableDocumentPictureInPicture: true,
        textTrackDisplay: { allowMultipleShowingTracks: false },
        playbackRates: [0.5, 1, 1.5, 2], 
        controlBar: { skipButtons: { forward: 10, backward: 10 } , volumePanel: {inline: false}},
        html5: {
            hls: {
                overrideNative: true
            }
        }
    });

    // 15번 확대 플러그인 적용
    const zoomPlugin = player.zoomPlugin({
        showZoom: true, // 줌 버튼
        showMove: true, // 이동 버튼
        showRotate: false, // 회전 버튼
        gestureHandler: true // 제스쳐로 줌, 드래그 가능
    });

    // 화질 조정 기능
    player.hlsQualitySelector({
        displayCurrentQuality: true,
        placementIndex: 1 // 메뉴 버튼 위치
    });

    const storedPlayPosition = localStorage.getItem('lastPlayPosition'); // localstarage에서 마지막 재생 위치 불러옴
    if (storedPlayPosition) {
        player.currentTime(storedPlayPosition); // 저장된 재생 위치가 있으면 그 위치로 시작
    }
    
    let isDragging = false; // 드래그 여부 
    let offset = [0, 0]; // 비디오 컨테이너 위치 변수


    // drag handle 드래그 기능
    dragHandle.addEventListener('mousedown', function (event) {
        isDragging = true;
        offset = [
            videoContainer.offsetLeft - event.clientX,
            videoContainer.offsetTop - event.clientY
        ];
    });

    document.addEventListener('mouseup', function () {
        isDragging = false;
    });

    document.addEventListener('mousemove', function (event) {
        if (isDragging) {
            videoContainer.style.left = `${event.clientX + offset[0]}px`;
            videoContainer.style.top = `${event.clientY + offset[1]}px`;
        }
    });
    



    // Picture-in-Picture event 리스너
    document.addEventListener('enterpictureinpicture', function () {
        previousPosition = {
            left: videoContainer.style.left,
            top: videoContainer.style.top
        };
        hideFloatingWindow();
    });

    document.addEventListener('leavepictureinpicture', function () {
        showFloatingWindow();
    });

    function hideFloatingWindow() {
        videoContainer.style.display = 'none';
    }

    function showFloatingWindow() {
        if (previousPosition) {
            videoContainer.style.left = previousPosition.left;
            videoContainer.style.top = previousPosition.top;
        }
        videoContainer.style.display = 'block';
    }

    // 유저가 페이지 떠나기 전 마지막 위치 저장
    window.addEventListener('beforeunload', function() {
        const lastPosition = player.currentTime();
        localStorage.setItem('lastPlayPosition', lastPosition);
    });

    // 비디오 종료 시 마지막 재생 위치 삭제
    player.on('ended', function() {
        localStorage.removeItem('lastPlayPosition');
    });

      
});


style.css

.floating-video-container {
    position: fixed;
    top: 450px; /* 초기 위치 지정 */
    left: 20px; /* 초기 위치 지정 */
    z-index: 1; /* z-index를 설정해 가장 위로. 단, 비교 대상이 다르면 항상 위가 아닐 수 있음!! 수치 주의! */
    background-color: #fff;
    padding: 3px;
    border-radius: 10px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}

.drag-handle {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 30px;
    cursor: move;
    background-color: #f0f0f0;
}

.vertical-split-container {
    display: flex;
    flex-direction: row;
    height: 100vh; 
}

.left-panel, iframe {
    width: 50%;
    height: 500px; 
    overflow: auto; 
}

.left-panel {
    padding: 20px; 
    border-right: 3px solid #ddd; 
}

iframe {
    border: none; 
}
profile
Full-Stack Dev / Data Engineer

0개의 댓글