video.js를 가지고 Floating window Video를 구현 시도 해봤다.
node.js를 서버로 두고 프론트 페이지만 프로토타입으로 빠르게 구현해봤다.
HTML5 기반의 오픈소스 비디오 플레이어 라이브러리.
웹에서 쉽게 재생할 수 있도록 도와주는 JavaScript 라이브러리.
npm install video.js
<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>
<!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>
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');
});
});
.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;
}