[유튜브 클론코딩] 9.2 Recording Video part.2

2021. 3. 18. 15:16Projects/유튜브 클론코딩

728x90
반응형

9.2 Recording Video part.2

지난시간에 데이터를 어떤 이유인지 얻어올 수 없었다.

그런데 알고보니 데이터는 레코딩이 다 끝나야 얻을 수 있다고 한다.

 

현재 start recording을 누르면 데이터는 recording 되는데, 아직 접근할 수 없다.

왜냐하면 디폴트로 MediaRecorder는 한번에 모든걸 저장하게 되어있기 때문이다.

그래서 dataavailable는 레코딩이 멈췄을 때 호출이 일어난다.

레코딩이 끝났을 때 전체 파일을 얻을 수 있다는 말이다.

 

🎁stopRecording 함수 만들기

 

const recordContainer = document.getElementById("jsRecordContainer");

const recordBtn = document.getElementById("jsRecordBtn");

const videoPreview = document.getElementById("jsVideoPreview");



let streamObject;

let videoRecorder;



const handleVideoData = (event) => {

    console.log(event);

}

const startRecording = () => {

    videoRecorder = new MediaRecorder(streamObject);

    videoRecorder.start();

    videoRecorder.addEventListener("dataavailable", handleVideoData);

    recordBtn.addEventListener("click", stopRecording);

};

const stopRecording = () => {

    videoRecorder.stop();

    recordBtn.removeEventListener("click", stopRecording);

    recordBtn.addEventListener("click", getVideo);

    recordBtn.innerHTML = "Start Recording";

}

const getVideo = async() => {

    try{

        const stream = await navigator.mediaDevices.getUserMedia({

            audio: true,

            video: { width: 1280, height: 720 }

        });

        videoPreview.srcObject = stream;

        videoPreview.muted = true;

        videoPreview.play();

        recordBtn.innerHTML = "Stop recording";

        streamObject = stream;

        startRecording();

    }catch(error){

        recordBtn.innerHTML = "😥 녹화할 수 없습니다."

    }finally{

        recordBtn.removeEventListener("click", getVideo);

    }

}



function init(){

    recordBtn.addEventListener("click", getVideo);

}

if(recordContainer){

    init();

}

흐름을 설명하자면, recordContainer가 존재하면 init 함수가 실행되면서 recordBtn를 클릭했을때 getVideo 함수가 실행된다.

 

getVideo는 비디오 접근 권한을 받아서 비디오의 프리뷰를 보여주고 recordBtn의 innerHTML은 stop recording으로 바뀌면서 startRecording 함수를 실행한다. 그리고 finally 구문에 의해 recordBtn을 클릭했을 때  getVideo함수를 실행하는 이벤트는 지워진다.

 

startRecording 함수는 videoRecorder를 가지고 레코딩을 시작하고 dataavailable일때 handleVideoData함수를 통해 콘솔에 event를 출력한다. dataavailable은 레코딩이 멈춰야 얻을 수 있다. 그리고 recordBtn을 클릭하면 stopRecording 함수를 실행하는 이벤트를 추가한다.

 

stopRecording은 videoRecorder를 멈춘다. 이 때 멈췄으므로 dataavailable 이벤트가 발생하여 handleVideoData함수에 의해 콘솔에 이벤트가 출력된다.

그리고 stopRecording은 레코딩을 멈추는 기능은 지우고 다시 getVideo 기능을 추가하고 innerHTML도 start recording으로 바꾼다.

 

(복잡한 것 같아도 차근차근 생각하면 어렵지 않다)

 

🎁녹화했을 때 발생하는 이벤트를 확인해보기

실제로 녹화해보면 콘솔에 찍히는 것을 확인해보자

 

여기엔 data가 있다. 이게 우리가 다운로드하고 싶은 것이다. 이것은 Blob 이벤트를 받아왔다.

 

즉 event.data에 우리가 원하는 파일이 담겨있다.

이것을 비구조화할당으로 예쁘게 표현해본다. data는 videoFile이라는 이름으로 받고 싶으므로 다음과 같이 작성한다.

 

const handleVideoData = (event) => {

    const { data:videoFile } = event;

}

 

🎁녹화한 파일을 다운로드할 수 있게 하기

이제 하고싶은 것은 이 videoFile을 다운로드 하는 것이다.

다운로드는 기본적으로 링크이다.

 

const handleVideoData = (event) => {

    const { data:videoFile } = event;

    const link = document.createElement("a");

    link.href = URL.createObjectURL(videoFile);

    link.download = "recorded.webm";

    document.body.appendChild(link);

    link.click();

}

 

자바스크립트에서 Blob(블랍)이란 이미지,사운드,비디오 등 멀티미디어 데이터를 다룰 때 사용 가능한 것으로 이진데이터를 담을 수 있다. 데이터 자체라기 보다는 데이터를 간접적으로 접근하기 위한 객체이며 URL.createObjectURL(블랍)을 이용해서 블랍 객체를 URL로 만들 수 있다.

 

우린 데이터 파일을 가지고 있고 우리는 a 링크를 엘리먼트로 하나 생성해서,

a링크의 href는 URL.createObjectURL(videoFile)로 만들고

download속성도 추가하고 파일명은 recorded.webm으로

(신기한게 여기서 파일명에 확장자까지 찍으니까 그 확장자로 저장이 된다 recorded.mp4라고 저장하면 mp4 파일로 저장이 된다!)

그리고 우리는 link를 body의 마지막 자식으로 삽입한다. 그리고 우린 페이킹 클릭을 할 것이다. (HTMLElement.click()메소드를 쓰면 엘리먼트의 클릭 이벤트가 실행된다.)

 

잘 작동한다. 레코딩을 시작하고, 레코딩을 끝내면 해당 파일이 자동으로 다운로드가 된다.

 

videoRecorder.js 의 최종 코드

const videoContainer = document.getElementById("jsVideoPlayer");
const videoPlayer = document.querySelector("#jsVideoPlayer video");
const playBtn = document.getElementById("jsPlayButton");
const volumeBtn = document.getElementById("jsVolumeButton");
const fullScreenBtn = document.getElementById("jsFullScreen");
const currentTime = document.getElementById("currentTime");
const totalTime = document.getElementById("totalTime");
const volumeRange = document.getElementById("jsVolume");

function handlePlayClick(){
    if(videoPlayer.paused){
        videoPlayer.play();
        playBtn.innerHTML = '<i class="fas fa-pause"></i>';
    }else{
        videoPlayer.pause();
        playBtn.innerHTML = '<i class="fas fa-play"></i>';
    }
}

function handleVolumeClick(){
    if(videoPlayer.muted){ //비디오가 음소거 되어 있으면
        videoPlayer.muted = false;  //음소거를 해제한다.
        volumeRange.value = videoPlayer.volume;
        if(volumeRange.value >= 0.6){
            volumeBtn.innerHTML = '<i class="fas fa-volume-up"></i>';
        }else if(volumeRange.value >= 0.3){
            volumeBtn.innerHTML = '<i class="fas fa-volume-down"></i>';
        }else if(volumeRange.value >= 0.0){
            volumeBtn.innerHTML = '<i class="fas fa-volume-off"></i>';
        }
        
    }else{ // 비디오가 음소거되어있지 않으면
        volumeRange.value = 0;
        videoPlayer.muted = true; // 음소거 상태로 만든다.
        volumeBtn.innerHTML = '<i class="fas fa-volume-mute"></i>' // 음소거 아이콘
    }
}
function exitFullScreen(){
    // document.exitFullscreen();//전체화면을 종료하기
    if(document.exitFullscreen){
        document.exitFullscreen();
    }else if(document.mozCancelFullScreen){
        document.mozCancelFullScreen();
    }else if(document.webkitExitFullscreen){
        document.webkitExitFullscreen();
    }else if(document.msExitFullscreen){
        document.msExitFullscreen();
    }
    fullScreenBtn.innerHTML = '<i class="fas fa-expand"></i>'; // 아이콘 변경하기(전체 화면 아이콘)
    fullScreenBtn.removeEventListener("click", exitFullScreen);
    fullScreenBtn.addEventListener("click", goFullScreen);
}

function goFullScreen(){
    if(videoContainer.requestFullscreen){
        videoContainer.requestFullscreen();
    }else if(videoContainer.mozRequestFullScreen){
        videoContainer.mozRequestFullScreen();
    }else if(videoContainer.webkitRequestFullscreen){
        videoContainer.webkitRequestFullscreen();
    }else if(videoContainer.msRequestFullscreen){
        videoContainer.msRequestFullscreen();
    }
    fullScreenBtn.innerHTML = '<i class="fas fa-compress"></i>'//아이콘 변경하기(작은 화면 아이콘)
    fullScreenBtn.removeEventListener("click", goFullScreen);
    fullScreenBtn.addEventListener("click", exitFullScreen);
}

const formatDate = (seconds) => {
    const secondsNumber = parseInt(seconds, 10);
    let hours = Math.floor(secondsNumber / 3600);
    let minutes = Math.floor((secondsNumber - hours * 3600) / 60);
    let totalSeconds = secondsNumber - hours * 3600 - minutes * 60;

    if(hours < 10){
        hours = `0${hours}`;
    }
    if(minutes < 10){
        minutes = `0${minutes}`;
    }
    if(totalSeconds < 10){
        totalSeconds = `0${totalSeconds}`;
    }
    return `${hours}:${minutes}:${totalSeconds}`;
}

function getCurrentTime(){
    currentTime.innerHTML = formatDate(Math.round(videoPlayer.currentTime));
}

function setTotalTime(){
    const totalTimeString = formatDate(videoPlayer.duration);
    totalTime.innerHTML = totalTimeString;
    setInterval(getCurrentTime, 1000);
}

function handleEnded(){
    videoPlayer.currentTime = 0;
    playBtn.innerHTML = '<i class="fas fa-redo"></i>';

}

function handleDrag(event){
    const { target: { value }} = event;
    videoPlayer.volume = value;
    if(videoPlayer.muted){ //비디오가 음소거 되어 있으면
        volumeBtn.innerHTML = '<i class="fas fa-volume-mute"></i>' 
    }else{
        if(value >= 0.6){
            volumeBtn.innerHTML = '<i class="fas fa-volume-up"></i>';
        }else if(value >= 0.3){
            volumeBtn.innerHTML = '<i class="fas fa-volume-down"></i>';
        }else if(value > 0.0){
            volumeBtn.innerHTML = '<i class="fas fa-volume-off"></i>';
        }
    }
}

function init(){
    videoPlayer.volume = 0.5;
    playBtn.addEventListener("click", handlePlayClick);
    volumeBtn.addEventListener("click", handleVolumeClick);
    volumeRange.addEventListener("input", handleDrag);
    fullScreenBtn.addEventListener("click", goFullScreen);
    videoPlayer.addEventListener("loadedmetadata", setTotalTime);
    videoPlayer.addEventListener("ended", handleEnded);
}

if(videoContainer){
    init();
}
728x90
반응형