본문으로 바로가기

Web-API의 하위 항목인 MediaDevices와 MediaRecorder를 사용해보겠습니다.

(https://darrengwon.tistory.com/230)

(https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder)

(https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices)

 

 

1️⃣ mediaDevices에 있는 getUserMedia 메서드사용하여 음성/영상 stream 데이터 생성

2️⃣ MediaRecorder를 이용해서 레코더를 실행, 정지를 컨트롤하고 stream을 blob으로 변환합니다.

 

 

The MediaDevices interface provides access to connected media input devices like cameras and microphones, as well as screen sharing. In essence, it lets you obtain access to any hardware source of media data.

 

즉, 영상 녹화 뿐만 아니라 화면 공유, 마이크 등 하드웨어를 사용하기 위한 것입니다만, 여기서는 영상 녹화 기능을 사용해보겠습니다.

 

 

속성 : EventTarget의 속성 그대로 상속함

 

이벤트 : devicechange (사용자 컴퓨터에 미디어 입출력 장치가 추가되거나 제거됐을 때 발생합니다.)

 

메서드

enumerateDevices() : 시스템에서 사용 가능한 미디어 입출력 장치의 정보를 담은 배열을 가져옵니다.

 

getSupportedConstraints()MediaStreamTrack 인터페이스가 지원하는 제약을 나타내MediaTrackSupportedConstraints 호환 객체를 반환합니다.

 

getDisplayMedia() : MediaStream으로 캡처해 공유나 녹화 용도로 사용할 화면 혹은 화면의 일부(창)를 선택하도록 사용자에게 요청합니다. MediaStream으로 이행하는 Promise를 반환합니다.

 

getUserMedia() : 사용자에게 권한을 요청한 후, 시스템의 카메라와 오디오 각각 혹은 모두 활성화하여, 장치의 입력 데이터를 비디오/오디오 트랙으로 포함한 MediaStream을 반환합니다. => 요놈을 주로 사용하게 됩니다.

 

스트림에 대해서는 다음 게시물을 참고합시다. (https://darrengwon.tistory.com/126)

 

[Buffer] 청크, 버퍼, 스트림

node.js에서 fs 모듈을 이용하면 종종 buffer를 마주칠 때가 있다. https://darrengwon.tistory.com/125 Node.js가 제공하는 내장 객체, 변수, 모듈 cf) node.js에는 window 전역 객체가 없다. global이 있다. __f..

darrengwon.tistory.com

 

 


 

 

하드웨어를 쓰려면 mediaDevices에 있는 getUserMedia 메서드 먼저 사용해야겠네요.

(https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)

MDN 문서에서 알려주는 사용법은 다음과 같습니다.

navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
  /* 스트림 사용 */
})
.catch(function(err) {
  /* 오류 처리 */
});

 

constraints란 제약을 말합니다. 다음과 같이 구성할 수 있습니다.

//모바일 기기의 후면 카메라와 마이크를 사용하겠다
{ audio: true, video: { facingMode: { exact: "environment" } } }

//마이크와 카메라를 사용하되 녹화되는 영상을 1280, 720 해상도로 촬영하겠다.
{
  audio: true,
  video: { width: 1280, height: 720 }
}

 

 

위 코드를 이용해서 다음과 같이 코딩했을 때, window.navigator.mediaDevices.getUserMedia는 MediaStream을 반환합니다.

div.record-container#jsRecordContainer
  video#jsVideoPreview(src="")
  button#jsRecordButton Start Recording
const recorderContainer = document.getElementById("jsRecordContainer");
const recordBtn = document.getElementById("jsRecordButton");
const videoPreview = document.getElementById("jsVideoPreview");

const startRecording = async () => {
  try {
  
    const stream = await window.navigator.mediaDevices.getUserMedia({
      audio: true,
      video: { width: 1280, height: 720 },
    });
    
    // getUserMedia의 결과물로 받은 스크림을 src로 지정
    videoPreview.srcObject = stream;
    
    // 녹화하는 즉시 실시간으로 재생하게끔
    videoPreview.play();
  } catch (erorr) {
    recordBtn.innerHTML = "😢 Can't record";
    recordBtn.removeEventListener("click", startRecording);
  }
};

function init() {
  recordBtn.addEventListener("click", startRecording);
}

if (recorderContainer) {
  init();
}

 

 

만약 parcel에서 실습을 진행하고 있다면 regeneratorRuntime is not defined 오류를 냅니다. 다음과 같이 import하면 오류가 해결됩니다.

import "regenerator-runtime/runtime";

 

 

영상 녹화를 위한 카메라 사용은 해결했으니 MediaRecorder를 이용해서 스트림을 저장하는 등 활용해봅시다.

(https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder)

 

문서를 살펴보면 MediaRecorder는 constructor를 통해 인스턴스를 만들고 해당 인스턴스에서 메서드와 속성을 빼다 쓰는 방법으로 활용합니다.

 

MediaRecorder constructor는 stream을 인자로 받으므로 앞서 작성한 코드의 stream을 넣어줍시다.

const startRecording = () => {
  const videoRecoder = new MediaRecorder(stream);
  // 녹화 시작
  videoRecoder.start();
};

 

 

videoRecoder를 콘솔로 찍어보면 videoRecoder의 속성들을 살펴볼 수 있습니다.

 

MediaRecorder.state, MediaRecorder.mimeType(mimetype 때문에 고생한 기억이...), MediaRecorder.stream

등등이 존재합니다. 방금 start()를 통해 녹화를 시작했으니 state는 recording이 뜨네요.

 

 

주의할 점은, 스트림을 받은 videoRecoder는 실시간으로 받은 정보를 뱉지 않고, 녹화가 중지된 이후에 뱉는다는 것입니다. stop() 메서드의 설명을 읽어보면

 

Stops recording, at which point a dataavailable event containing the final Blob of saved data is fired. No more recording occurs.

 

따라서 5초 뒤에 stop() 메서드를 통해 비디오 재생을 멈춘 후 발생하는 blob 이벤트를 출력해보겠습니다.

const startRecording = () => {
  const videoRecoder = new MediaRecorder(streamObject);
  videoRecoder.start();
  setTimeout(() => videoRecoder.stop(), 5000);
  videoRecoder.addEventListener("dataavailable", (e) => console.log(e));
};

 

✔ 위와 같은 방법은 예시일 뿐이고, 실제로는 start 메서드의 인자로 timeslice를 넣어주는 것이 좋습니다.

videoRecoder.start();

Begins recording media; this method can optionally be passed a timeslice argument with a value in milliseconds. If this is specified, the media will be captured in separate chunks of that duration, rather than the default behavior of recording the media in a single large chunk.

즉, timeslice 마다 청크를 만든다는 것입니다. 지정하지 않으면 녹화본이 통째로 청크가 됩니다.

videoRecoder.start(1000);

 

이벤트의 이름은 blobevent이고 해당 이벤트의 여러 속성들이 출력되네요.

blob의 영단어 뜻 그대로 덩어리입니다.

blob에서 대해서는 (https://developer.mozilla.org/en-US/docs/Web/API/Blob) 이 문서를 참고합시다.

 

여기서 중요한 건 data입니다. data에 우리가 녹화한 영상이 있습니다.

 

 

 

그렇다면 특정 버튼을 클릭하면 녹화를 시작하고, 다시 누르면 녹화를 중단하는 등의 코드는 이미 작성했다 가정하고

(addEventListener, removeEventListener를 적절히 활용하고, blob 데이터를 받기 위해 play시 timeslice를 적어주면 됩니다.)

 

녹화되는 도중 넘어오는 blob을 어떻게 처리하는지를 알아봅시다.

 

// 녹화가 완료된 후 handleVideoData 콜백 실행
videoRecoder.addEventListener("dataavailable", handleVideoData);
const handleVideoData = (e) => {
  // blob 이벤트에서 data 추출
  const { data } = e;
  
  // 다운로드를 위해 a 태그를 만들어주고 href로 해당 data를 다운로드 받을 수 있게 url을 만듭시다
  const videoDownloadlink = document.createElement("a");
  videoDownloadlink.href = URL.createObjectURL(data);
  
  // 다운로드 되는 파일의 이름. 확장자는 mp4 등 다양하게 가능하지만 오픈 소스인지 확인 합시다
  link.download = "recorded.webm";
  
  // body에 append 해줘야겠죠
  document.body.appendChild(link);
  
  // faking click. body에 append 했으니 클릭해서 다운로드를 해줘야 합니다.
  link.click();
};

 

짜자자잔~

 

webm은 오픈 소스라서 deploy이 문제가 될 것이 없지만 mp4는 라이센스를 취득해야 한다고 합니다

 

 

* 추가

 

timeslice를 지정한 후 blob이 조각난 경우, 이 blob을 하나로 합쳐서 하나의 파일로 만들어주는 작업을 진행해야 합니다.

 

 

// blob이 발생할 때마다 audioData에 넣어줌
let audioData = [];

// 모든 blob을 배열 형태로 넘겨주면 알아서 합침
let fullBlob = new Blob(audioData, {
  mimeType: "audio/webm;codecs=opus",
});

 

* 음성 녹화를 구현해보았습니다.

 

import "./styles.css";
import "regenerator-runtime/runtime";

const RecodingButton = document.querySelector("button");
const recodingTime = document.querySelector(".recodingTime");

let time = 0;
let audioData = [];
let mediaRecorder;

const onRecodingStopClick = function (e) {
  // 우선 녹화를 멈춥니다.
  mediaRecorder.stop();
  // 정지 이벤트를 지우고 재녹화 이벤트를 달아줍니다.
  RecodingButton.removeEventListener("click", onRecodingStopClick);
  RecodingButton.addEventListener("click", onRecodingClick);

  // mediaRecorder 이벤트 재 실행을 막습니다
  mediaRecorder.removeEventListener("dataavailable", saveBlobData);

  // 텍스트 변화
  RecodingButton.innerHTML = "Start Recoding";
  recodingTime.innerHTML = "";
  time = 0;
  console.log(audioData);

  // 받은 audioData를 전부 합쳐서 하나의 Blob으로 만듦
  let fullBlob = new Blob(audioData, {
    mimeType: "audio/webm;codecs=opus",
  });

  audioData = [];

  // // 다운로드
  const audioDownloadLink = document.createElement("a");
  audioDownloadLink.href = URL.createObjectURL(fullBlob);
  audioDownloadLink.download = "Audio.webm";
  document.body.appendChild(audioDownloadLink);
  audioDownloadLink.click();
};

const saveBlobData = (e) => {
  time++;
  recodingTime.innerHTML = `Recoding for ${time}`;
  console.log(e.data);
  audioData.push(e.data);
};

const onRecodingClick = function (e) {
  // stream을 만들어냅니다.
  navigator.mediaDevices
    .getUserMedia({ audio: true })
    .then((stream) => {
      // mediaRecoder로 녹음을 시작합니다.
      mediaRecorder = new MediaRecorder(stream, {
        mimeType: "audio/webm;codecs=opus",
      });

      // 녹화가 시작되었으므로 버튼에서 녹화 이벤트를 제거하고 녹화 중지 이벤트를 달아줍니다.
      RecodingButton.removeEventListener("click", onRecodingClick);
      RecodingButton.addEventListener("click", onRecodingStopClick);

      // 버튼의 글을 바꿉니다.
      RecodingButton.innerHTML = "Stop Recoding";

      // 녹화 시작. 1초마다 chunk를 생성
      mediaRecorder.start(1000);

      // start에 설정한 timeslice에 따라 1초 마다 dataavailable한 blob 추출됨
      mediaRecorder.addEventListener("dataavailable", saveBlobData);
    })
    .catch((err) => {
      RecodingButton.innerHTML = "Error!";
      RecodingButton.removeEventListener("click", onRecodingClick);
    });
};

RecodingButton.addEventListener("click", onRecodingClick);
import "./styles.css";

const button = document.querySelector("button"),
  body = document.querySelector("body"),
  counter = document.querySelector(".counter");
let isRecording = false;
let stream = null;
let mediaRecorder = null;
let chunks = [];
let count = 0;
let countInterval = null;

navigator.mediaDevices
  .getUserMedia({
    audio: true
  })
  .then(streamObj => (stream = streamObj))
  .catch(err => console.log(err));

const startCounting = () => {
  counter.innerHTML = `Recording for ${count}`;
  count++;
};

const handleStreamData = e => {
  const { data } = e;
  chunks.push(data);
};

const handleStreamStop = () => {
  const audio = new Blob(chunks, { type: "audio/webm;codecs=opus" });
  const url = URL.createObjectURL(audio);
  const link = document.createElement("a");
  button.innerHTML = "Start Recording";
  clearInterval(countInterval);
  count = 0;
  counter.innerHTML = "";
  body.appendChild(link);
  link.href = url;
  link.download = "Audio";
  link.click();
};

const startRecording = () => {
  if (stream) {
    mediaRecorder = new MediaRecorder(stream);
    mediaRecorder.start();
    mediaRecorder.addEventListener("dataavailable", handleStreamData);
    mediaRecorder.addEventListener("stop", handleStreamStop);
    startCounting();
    countInterval = setInterval(startCounting, 1000);
  }
};

const toggleState = () => {
  if (isRecording) {
    button.innerHTML = "Start Recording";
    mediaRecorder.stop();
  } else {
    button.innerHTML = "Stop Recording";
    startRecording();
  }
  isRecording = !isRecording;
};

button.addEventListener("click", toggleState);

darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체