본문 바로가기
공부/html & javascript

[Screen Capture API] javascript로 화면을 동영상으로 캡쳐 후 저장까지

by 고기 2023. 3. 6.

글 내용 정리

1. screen capture api 소개

2. navigator.mediaDevices undefined 문제

3. 로컬에서 ssl 인증서 설치하기

4. 캡쳐한 동영상 mp4로 다운로드 하기

5. 사용방법

6. 문제점


지금 진행중인 프로젝트에서 필요한 기능을 찾다가 재밌는 기능이 있어서 한 번 사용해봤다.

하려는 프로젝트의 내용은 여기서 확인하면 되고...

https://1545154.tistory.com/99

 

[save image (format : gif)] html에서 만든 div element를 gif 이미지로 저장할 수 없을까?

글 내용 정리 1. 소개 2. 실패 리스트 3. gif 변환 방법 소개 4. 작동결과 1. 소개 결론부터 말하면 일반적인 방법으로는 저장할 수 없다. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 대신에 대충

1545154.tistory.com

 

최근에 네이버 코딩 테스트를 볼 기회가 생겼다.

신입공채 뽑길래 서류넣고 코테... 그리고 광탈

 

테스트중에 모니터 화면공유를 해야 했는데 그게 이 screen capture api를 사용해서 이루어지고 있었다.

아마.. 맞을걸? 아니면 부끄러운데... 근데 뭐 아니어도 비슷한 기능이겠지!

아무튼 별 것 아니지만 이전에 공부했던걸 일상에서(?) 마주치게 되면 꽤 뿌듯하다니까.

 

1. screen capture api 소개

기능 자체는 심플하다.

https://developer.mozilla.org/en-US/docs/Web/API/Screen_Capture_API/Using_Screen_Capture

 

Using the Screen Capture API - Web APIs | MDN

In this article, we will examine how to use the Screen Capture API and its getDisplayMedia() method to capture part or all of a screen for streaming, recording, or sharing during a WebRTC conference session.

developer.mozilla.org

 

예제를 참고해서 screen capture api 코드를 작성하고 화면캡쳐를 해보면 다음과 같이 실행된다.

 

캡쳐하고 싶은 창을 선택하고 공유 버튼을 클릭하면 캡쳐를 보여줄 영역에 뙇 하고 보이는데, 보다시피 화질감소도 딱히 없고 프레임도 안 끊긴다.

근데 가장 중요한 건 이걸 어떻게 써야하냐는 거지... 

 

2. navigator.mediaDevices undefined 문제

이걸 사용하기 앞서 문제가 될 만한 부분이 3가지 있다.

 

첫 번째는 보안문제다.

이 기능을 쓰려면 https 인증을 받아야한다. 안그러면 기능 못 쓴다.

개인적으로 쓸려면 상관없으나 웹 서비스를 운영하려면 도메인 구매하고 ssl 인증 후 https를 달아야만 사용할 수 있다는 점이다. 근데 지금 생각해보니 원래 웹 서비스 운영하려면 저 정도는 해야하지 않나? 별 문제 없을 것 같기도 하다.

 

본인 화면에서 뭘 털어갈게 있겠냐 싶지만서도... 위에서 창을 선택하는 사진을 보다시피 이게 개인 화면을 여과없이 공유할 수 있다보니 보안에 취약할 수 밖에 없다.

그거랑 별개로 어떤식으로 한 건지 모르겠지만 카카오톡 같은 경우 흰색으로 모자이크 처리를 해주긴 한다.

 

아무튼 https 인증을 받지 않으면 이런식으로 크롬이나 웨일 등 브라우저에서 mediaDevices.getDisplayMedia라는 객체를 제공하지 않아서 사용할 수 없다.

 

whale://flags/#unsafely-treat-insecure-origin-as-secure 여기서 보안정책 무시하기 하면 된다는데,,, 나는 멍청해서 못찾겠더라고. 뭐 이렇게 하라고만 나와있지 실제 화면 찍어놓은 사람들이 없어서 되는건지 모르겠다.

 

3. 로컬에서 ssl 인증서 설치하기

그래서 두 번째 문제는 이 기능을 사용하기 위해 도메인 사서 https 인증을 받아야 한다는거다.

근데 그러기에는 과하니까 self-signed certificate라고 로컬에서 ssl 적용을 시켜서 개발용으로 쓸 수 있는데 그걸 사용했다.

 

해당 링크 참고해서 ssl 인증서를 설치하자.

https://1545154.tistory.com/109

 

[ssl 인증서 설치] 로컬에서 사용할 테스트용 ssl인증서 설치하기

1. 로컬에서 ssl 인증서 설치하기 screen capture api를 사용하기 위해 스프링 부트 로컬 프로젝트에 https를 적용하는 과정이다. 프로젝트 내용은 해당 링크 참고하면 된다. https://1545154.tistory.com/100 [Scre

1545154.tistory.com

 

4. 캡쳐한 동영상 mp4로 다운로드 하기

마지막으로 세 번째 문제는 가장 중요한데, 옵션을 좀 더 찾아봐야겠지만 이렇게 캡쳐한 화면을 파일로 저장을 할 수 있는 방법을 못 찾겠다는 거다...

명색이 screen capture api면서 화면을 저장할 수 있는 기능이 없을까 싶긴한데......마저 찾아보고나서 있으면 리뷰해보겠다. 

 

전반적인 다운로드 기능 관련 설명은 여기 참고하면 된다.

https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API/Recording_a_media_element

 

Recording a media element - Web APIs | MDN

While the article Using the MediaStream Recording API demonstrates using the MediaRecorder interface to capture a MediaStream generated by a hardware device, as returned by navigator.mediaDevices.getUserMedia(), you can also use an HTML media element (name

developer.mozilla.org

 

일단 테스트를 위해 예제를 따라서 비슷한 폼으로 만들어볼건데... 약간 변형했기 때문에 참고만 하면 된다.

근데 변형했다고 말한거치곤 핵심코드는 걍 빼다박아놓긴했다 ㅋㅋ

코드 리뷰는 주석으로 대체한다.

 

1) html element 추가

// 프로필 생성 테스트 test4
// javascript.jsp
function testFFF(values){

    console.log("makeVideo test");

    $("#characterEquipInfo2").empty();
    html = '';

    html += "<div id='characterDetailSub'>"
    html +=     "<h2>캐릭터 프로필</h2>"
    html +=     "<div class='characterStat'>"
    html +=     "<img src='layout/images/gif/gif5.gif' alt='이미지'/>"
    html +=     "<img src='layout/images/pic1.jpg' alt='이미지'/>"
    html +=     "<img src='layout/images/weapon.png' alt='이미지'/>"
    html +=     "<img src='layout/images/test.gif' alt='이미지'/>"
    html +=     "<div class='characterDetail'>wwwww</div>"
    html +=     "</div>"
    html += "</div>"
    html += "<button id='saveGif' onClick=screenshot()>다운로드</button>";
    html += "<button id='closed' onClick=closed()>close</button>";

    /**************************************/
    // 참고할 코드는 여기부터 //
    // 원 예시에서는 listener로 핸들링하고 있는데, 
    // 자꾸 값 안불러와지고 안돼는게 많아서 함수 호출로 변경했다.
    html += "<div class='left1'>"
    html +=   "<button id='startButton' onClick=st1Recording()>Start Recording</button>";
    html +=   "<h2>Preview</h2>"
    html +=   "<video id='preview' width='160' height='120' autoplay muted></video>"
    html += "</div>"

    html += "<div class='right1'>"
    html +=  "<button id='stopButton' onClick=st2Recording()>Stop Recording</button>";
    html +=   "<h2>Recording</h2>"
    html +=   "<video id='recording' width='160' height='120' controls></video>"
    html +=   "<a id='downloadButton' class='button'> Download </a>"
    html += "</div>"
    /**************************************/
    
    $("#characterEquipInfo2").append(html);

    location.href = '#characterEquipInfo2';
}

 

이런식으로 html 구조가 나오면 되는데, element 배치는 대충 둡시다.

 

2) 해당 함수 작성

// startButton function
function st1Recording(){
    /***************************************************/
    // (1) 변수 세팅
    // 찍고있는 영상
    let preview = document.getElementById("preview");
    // 완료된 영상
    let recording = document.getElementById("recording");
    // 시작버튼
    let startButton = document.getElementById("startButton");
    // 종료버튼
    let stopButton = document.getElementById("stopButton");
    // 다운로드버튼
    let downloadButton = document.getElementById("downloadButton");    
    // 녹화시간(5초)
    let recordingTimeMS = 5000;
    /***************************************************/    
    console.log("+++", recordingTimeMS, "+++");
    
    /********************************/
    // (2) navigator.mediaDevices
    // 예제에서는 navigator.mediaDevices.getUserMedia 
    // navigator.mediaDevices.getDisplayMedia로 변경
    // getUserMedia -> 카메라로 캡쳐(노트북일경우 흐리멍덩한 본인 얼굴 찍혀서 눈갱당하니 조심^^)
    // getDisplayMedia -> 컴퓨터 화면을 캡쳐
    /********************************/
    navigator
          .mediaDevices
          .getDisplayMedia({ 
                        /********************************/
                        // (3) displayOptions
                        // audio는 소리... 필요없으니까 false
                        // perferCurrentTab은 현재 페이지 세팅해서 공유탭 안나오나 싶어서 넣어봤는데...
                        // 현재 페이지 공유할 수 있는 탭을 만들어주기만함 => 왕쓸모없음
                        // 값 true/false 설정해서 직접 실행시켜보기
                        video: true
                        , audio: false
                        , preferCurrentTab:true
                        /********************************/
                         })
          .then((stream) => {
            /**********************/
            // (4) stream object settings
            // 구체적인 작동방식은 예제 페이지를 확인해보자...            
            /**********************/
            preview.srcObject = stream;
            downloadButton.href = stream;
            preview.captureStream =
            preview.captureStream || preview.mozCaptureStream;
            return new Promise((resolve) => (preview.onplaying = resolve));
          })
          .then(() => startRecording(preview.captureStream(), recordingTimeMS))
          .then((recordedChunks) => {
            /*****************************/
            // 5) blob settings
            // type -> video/mp4 
            // src/href/download tag settings
            /*****************************/
            let recordedBlob = new Blob(recordedChunks, { type: "video/mp4" });
            recording.src = URL.createObjectURL(recordedBlob);
            downloadButton.href = recording.src;
            downloadButton.download = "RecordedVideo.mp4";

            console.log(
              `Successfully recorded ${recordedBlob.size} bytes of ${recordedBlob.type} media.`
            );
          })
          .catch((error) => {
            if (error.name === "NotFoundError") {
              console.log("Camera or microphone not found. Can't record.");
            } else {
              console.log(error);
            }
          });
}

function startRecording(stream, lengthInMS) {
  /*********************************/
  // record start
  /*********************************/
  let recorder = new MediaRecorder(stream);
  let data = [];

  recorder.ondataavailable = (event) => data.push(event.data);
  recorder.start();
  console.log(`${recorder.state} for ${lengthInMS / 1000} seconds…`);

  let stopped = new Promise((resolve, reject) => {
    recorder.onstop = resolve;
    recorder.onerror = (event) => reject(event.name);
  });

  /*********************************/
  // record stop ... [recordingTimeMS/1000]초
  /*********************************/
  let recorded = wait(lengthInMS).then(() => {
    if (recorder.state === "recording") {
      recorder.stop();
    }
  });

  return Promise.all([stopped, recorded]).then(() => data);
}

// wait()
function wait(ms = 1000) {
  return new Promise((resolve) => setTimeout(resolve, ms));

}

// stopButton function
function st2Recording(){
    /*********************************/
    // 공유 중단
    // 5. 사용방법 참고
    /*********************************/
    stop(preview.srcObject);
}

function stop(stream) {
  stream.getTracks().forEach((track) => track.stop());
}

 

5. 사용방법

1) start recording 버튼 클릭

캡쳐할 화면을 선택 후 공유 버튼을 누르면 캡쳐가 시작된다.

 

2) 캡쳐시작

preview -> 캡쳐중인 화면 확인

 

3) 캡쳐종료

stop recording -> 공유 중단

recording -> 캡쳐 완료된 동영상 확인

 

4) 다운로드

download -> 파일 다운로드

 

6. 문제점

이 기능을 사용하기에는 큰 문제가 있다.

코드도 간단하고 속도도 빠른데 무엇보다 산출물의 해상도 저하가 없어서 개인이 사용하기에는 더 없이 좋은 기능이다.

 

하지만 웹을 배포해서 다른 사용자가 이걸 사용할경우 문제가 생긴다.

Screen Capture Api는 보안정책으로 "무조건" 녹화를 시작할 페이지를 선택해야하기 때문에 녹화할 페이지를 사용자가  직접 선택해야 하는데, 이 말은 사용자가 어떤 페이지를 선택할 지 알 수 없다는 뜻이다.

선택해야 하는 페이지를 공지해주면 되겠지만 결국 페이지를 선택하는 건 사용자의 선택에 맡기는거라서 프로그램을 배포하려는 입장에서 보면 굉장히 중요한 문제일 수 있다.

 

일단 첫 번째로 사용자 입장에서 생각해봤을 때 굉장히 불편하다.

사용할 사람들이 어떻게 느낄지는 모르겠지만 적어도 나는 불편했다.

 

두 번째는 다른 탭을 선택했을 때 발생되는 문제이다.

사실 본인이 개발한 프로그램이니까 해당 탭을 선택해야 한다는 사실을 알 수 있지만 처음 프로그램을 사용하는 사람들의 경우 당연히 모를 수 밖에 없다.

현재 탭 옵션(preferCurrentTab:true)을 줘서 다운로드 할 탭을 표시해줄 수 있지만 어차피 그 뿐이다.

청개구리마냥 호기심으로 다른 탭을 선택하는 사람도 있을 수도 있는데... 뭐 그 정도는 애교고, 중요한건 선택한 탭에 대해 후처리... 추가적인 프로세스가 있을 경우 치명적인 오류가 발생할 수 있다.

 

세 번째는 역시 보안문제다.

일단 기능을 사용하려면 카메라와 마이크 권한을 허용해야한다. 애초에 여기서부터 문제다.

보안문제라고 하니까 아무것도 모르는 사람들의 입장에서는 당연히 사용하기 꺼려진다.

 

그런 문제들이 있어서 해당 기능은 프로젝트에 적용하지 않는 쪽으로 결론을 냈다.

같은 문제로 스오플 등에 비슷한 질문들이 올라왔었다.

이 답변으로 해결이 될지는 모르겠는데 적어도 내가 봤을 땐 근본적인 해결방법은 아니다 ㅋㅋㅋ

https://stackoverflow.com/questions/63956289/is-it-possible-to-use-navigator-getdisplaymedia-without-user-authentication

 

Is it possible to use navigator.getDisplayMedia without user authentication?

navigator.mediaDevices.getDisplayMedia({ audio: false, video: true }).then(gotMedia).catch(function(e) { console.log('getDisplayMedia() error: ', e); }); when executing the above c...

stackoverflow.com

 

깃허브에서 논의중인 내용이다.

getViewportMedia기능을 추가하는거에 대한 내용이고, 이게 끝났는지 진행중인지는 모르겠지만 아무튼 현재는 못 쓴다.

https://github.com/w3ctag/design-reviews/issues/625

 

API for display-capturing the current tab · Issue #625 · w3ctag/design-reviews

Ya ya yawm TAG! I'm requesting a TAG review of getCurrentBrowsingContextMedia. Overview Consider the existing navigator.mediaDevices.getDisplayMedia(). It allows a user unlimited choice of sources ...

github.com

 

아무튼 screen capture api 리뷰는 여기까지...!

댓글