본문 바로가기
Develop/Zoom만들기

Zoom 만들기 12. 비디오와 오디오 제어하기

by 보보트레인 2023. 9. 5.

영상, 소리와 같은 미디어를 스트림으로 받아오는 작업을 해봤는데, 우리가 생성한 스트림 안에 있는 트랙(Track)이라는 것을 사용하면 비디오와 오디오를 따로 제어할 수 있음.

 

1. 트랙 살펴보기

스트림에서 트랙이란 스트림을 구성하는 미디어 요소 하나하나를 구분하는 일종의 단위이다.

→ 각 트랙은 배열로 표현되는데, 배열을 구성하는 객체에는 kind라는 키가 있고 여기에 트랙의 종류가 표시돼.

 

ex) MediaStreamTrack {kind: 'audio'}  /  MediaStreamTrack {kind: 'video'}  


2. 비디오와 오디오 제어하기

두 버튼의 핸들러 함수로 가서 버튼을 각각 클릭할 때마다 enable의 값이 변경되도록 변경.

const socket = io();
const myFace = document.getElementById("myFace");
const muteBtn = document.getElementById("mute");
const cameraBtn = document.getElementById("camera");

let myStream;
let muted = false;
let cameraOff = false;

async function getMedia() {
    try {
        myStream = await navigator.mediaDevices.getUserMedia({
            audio: true,
            video: true,
        });
        myFace.srcObject = myStream;
    } catch(e) {
        console.log(e);
    }
}

getMedia();

function handleMuteClick() {
    myStream.getAudioTracks()
    .forEach((track) => (track.enabled = !track.enabled));
    if(!muted){
        muteBtn.innerText = "Unmute";
        muted = true;
    }else{
        muteBtn.innerText = "Mute";
        muted = false;
    }
}

function handleCameraClick() {
    myStream.getVideoTracks()
    .forEach((track) => (track.enabled = !track.enabled));
    if(!cameraOff){
        cameraBtn.innerText = "Turn Camera On";
        cameraOff = true;
    }else{
        cameraBtn.innerText = "Turn Camera Off";
        cameraOff = false;
    }
}

muteBtn.addEventListener("click", handleMuteClick);
cameraBtn.addEventListener("click", handleCameraClick);

설명 : forEach매서드를 이용해 배열의 요소에 접근하고, 거기에서 enabled의 값을 변경해주었음.

enabled는 boolean데이터니까 논리연산자인 not(!)으로 값을 반전시켜주면 됨.

 

<결과화면>

눈에 보이는 카메라 버튼을 클릭해보자

클릭 전, 카메라 잘 나옴
클릭 후, 카메라 꺼짐

 


3. 카메라 목록 만들기

카메라가 여러대인 경우 카메라를 전환해주는 기능도 있으면 좋음.

따라서 사용자 기기에 장착된 모든 미디어 기기를 가져온 다음, 거기서 카메라에 해당하는 것들만 골라내서 접근.

 

→ 일단 모든 카메라 목록을 가져오는 것 부터 해볼 예정이다.

const devices = await navigator.mediaDevices.enumerateDevices();

nevigator.mediaDevices.enumerateDevices(); 라는메서드가 출현했다.

모든 기기의 정보를 가져와 주는 메서드이다. (enumerate = 열거하다)

→ 이를 이용하여 카메라 배열을 만들어보자.

 

async function getCameras() {
    try {
       const devices = await navigator.mediaDevices.enumerateDevices();
       const cameras = devices.filter((device) => device.kind === "videoinput");
    } catch(e) {
        console.log(e);
    }
}

filter 메서드를 통해 kind가 videoinput인 미디어 기기만으로 구성된 cameras 배열을 만들었음

여기서 videoinput은 컴퓨터와 연결된 카메라 기기들을 의미함. 

즉, cameras 배열에 videoinput에 담긴 카메라 데이터들을 모두 담았다는 뜻.

 

※ 실제로 console에 console.log(device)로 찍어보면 videoinput이 카메라의 kind(종류)를 뜻하는 데이터임을 알 수 있다.

 

뷰엔진도 고쳐보자.

doctype html

html(lang="en")
    head
        meta(charset="UTF-8")
        meta(http-equiv="X-UA-Compatible", content="IE=edge")
        meta(name="viewport", content="width=device-width, initial-scale=1.0")
        title Zoom
        //MVP.css는 우리가 태그에 class나 id같은 특성을 추가하지 않아도 자동으로 스타일을 적용해주는 라이브러리다.
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
    body
        //h1 It works!
        header
            h1 Zoom
        main
            div#myStream
                video#myFace(autoplay, playsinline, width="400", height="400")
                button#mute Mute
                button#camera Turn Camera Off
                select#cameras
        script(src="/socket.io/socket.io.js")
        script(src="/public/js/app.js")

select#cameras를 추가하여  드롭다운 메뉴를 만들었음.

보통 select 안에는 하위요소로 option이 포함되어야 하지만, 우리는 이를 자바스크립트로 추가해 줄 것임.

→ 그래야 카메라 개수가 달라져도 그에 맞게 대응할 수 있다.

 

app.js에 다음의 코드를 추가.

const cameraSelect = document.getElementById("cameras");
async function getCameras() {
    try {
       const devices = await navigator.mediaDevices.enumerateDevices();
       const cameras = devices.filter((device) => device.kind === "videoinput");
       cameras.forEach((camera) => {
        const option = document.createElement("option");
        option.value = camera.deviceId;
        option.innerText = camera.deviceId;
        cameraSelect.appendChild(option);
       });
    } catch(e) {
        console.log(e);
    }
}

설명 : option요소에는 선택한 option에서 실제 처리할 값을 뜻하는 value라는 속성이 있다.

여기에는 카메라의 deviceId를 각각 지정해 두었음. (우리가 실제로 카메라를 제어할 때, 필요한 값은 기기의 id임!)

→ 드롭다운 메뉴의 '옵션'역할을 하는 element를 만들고 거기에 각각의 카메라 데이터들의 deviceId를 따로 뽑음

→ 뽑은 내용을 option에 담고 cameraSelect에 가져다 붙힘으로써 뷰 엔진에도 잘 출력되게 해놓음.  

 

label은 그저 모델명을 표시하는 역할일 뿐이다.

 

<결과 확인>

드롭다운 메뉴에 연결된 카메라 목록이 잘 나온것을 확인할 수 있다. 필자는 카메라가 1개라서... ㅎㅎ 1개만 뜬다.


4. 카메라 변경하기 

다음의 코드를 app.js에 추가

function handleCameraChange() {
    console.log(cameraSelect.value);
}
cameraSelect.addEventListener("input", handleCameraChange);

input이벤트는 입력값이 변경될 때 발생할 것임. 이를 의미하는 함수를 handleCameraChange로 선언하였다.

여기에서는 선택한 option의 value속성을 가져오게 했는데, value속성은 deviceId를 의미한다.

deviceId를 사용해야 카메라를 변경할 수 있으므로 가져오는 데이터가 무엇인지 정확하게 이해하고 있어야 한다.

 

카메라가 변경 시, 변경된 카메라의 재적용을 위해 해당 카메라의 영상을 다시 받아오기 위해서 스트림을 다시 시작해야 함. 이미 getMedia가 정의되어 있으니 이를 재사용하면 끝임.

 

하지만 특정 카메라를 사용하도록 추가적인 제약이 필요함 ( 우리가 선택한 카메라로 실행되도록.. )

getMedia 함수 안에서 getUserMedia 메서드를 호출할 때, 인자로 전달되는 constraints(제약)를 적절하게 설정

 

(내가 생각한 방법)

select요소를 통해 선택된 deviceId에 해당하는 카메라 deviceId가 필요.

→ getMedia함수에 deviceId를 넘겨주어 constraints를 재설정 하자.

 

const socket = io();
const myFace = document.getElementById("myFace");
const muteBtn = document.getElementById("mute");
const cameraBtn = document.getElementById("camera");
const cameraSelect = document.getElementById("cameras");

let myStream;
let muted = false;
let cameraOff = false;

async function getCameras() {
    try {
       const devices = await navigator.mediaDevices.enumerateDevices();
       const cameras = devices.filter((device) => device.kind === "videoinput");
       cameras.forEach((camera) => {
        const option = document.createElement("option");
        option.value = camera.deviceId;
        option.innerText = camera.deviceId;
        cameraSelect.appendChild(option);
       });
    } catch(e) {
        console.log(e);
    }
}


async function getMedia(deviceId) {
    const initialConstraints = {
        audio: true,
        video: { facingMode: "user" }
    };
    const cameraConstraints = {
        audio: true,
        video: { deviceId: {exact: deviceId} }
    };

    try {
        myStream = await navigator.mediaDevices.getUserMedia(
            deviceId ? cameraConstraints: initialConstraints
        );
        myFace.srcObject = myStream;
        if(!deviceId){
            await getCameras();
        }
    } catch(e) {
        console.log(e);
    }
}

getMedia();

function handleMuteClick() {
    myStream.getAudioTracks()
    .forEach((track) => (track.enabled = !track.enabled));
    if(!muted){
        muteBtn.innerText = "Unmute";
        muted = true;
    }else{
        muteBtn.innerText = "Mute";
        muted = false;
    }
}

function handleCameraClick() {
    myStream.getVideoTracks()
    .forEach((track) => (track.enabled = !track.enabled));
    if(!cameraOff){
        cameraBtn.innerText = "Turn Camera On";
        cameraOff = true;
    }else{
        cameraBtn.innerText = "Turn Camera Off";
        cameraOff = false;
    }
}

async function handleCameraChange() {
    await getMedia(cameraSelect.value);
}

muteBtn.addEventListener("click", handleMuteClick);
cameraBtn.addEventListener("click", handleCameraClick);
cameraSelect.addEventListener("input", handleCameraChange);

설명 : 처음 페이지가 열리고 getMedia 함수가 호출될 때는 특정 deviceId를 선택하기 전임. ( = initialConstraints 설정 )

따라서 deviceId가 존재한다는 뜻은 특정 카메라를 선택했다는 뜻. ( = cameraConstraints를 설정하여 제약 )

  • getMedia() 상태로 함수가 실행되면 initialConstraints가 실행됨
  • getMedia(deviceId)상태로 함수가 실행되면 cameraConstraints 가 실행됨

※ 여기서 video: { facingMode: "user" } 는 모바일 장치의 전면 카메라를 요청하는 코드임. 모바일이 아닌 PC에서는 내장된 기본 카메라를 선택하게 됨.

 

추가로 매번 카메라를 바꿀때마다 option에 deviceId를 추가하는데 여러번 중복추가하는 버그를 없애기 위해 

if(!deviceId) {} 코드를 통해 select요소의 항목이 계속해서 늘어나지 않도록 제한했다.

 

※ 카메라를 바꿀 때 마다 getMedia() 함수가 호출되므로 getCameras 함수가 호출됨.

하지만 우리의 요구사항은 맨처음 getCameras의 단독 호출때는 목록 추가가 되면서,

카메라를 바꾸는 추가 상황인 getMedia()일때마다 호출되지는 getCameras는 목록추가가 제한되어야함.

따라서 getMedia의 인자인 deviceId가 아닌경우에만 카메라 목록을 추가하도록 제한 코드를 넣은 것.

  • getMedia() 상태로 함수가 실행되면 getCameras 함수가 호출됨( 초기 세팅 )
  • getMedia(deviceId)상태로 함수가 실행되면 getCameras 함수가 호출되지 않음 ( 중복 생성 제한 )

 

반응형