# 3.0 -2 User Video

이원규·2022년 6월 17일

Zoom clone coding

목록 보기
15/18

1. User Video

  • 비디오 스트리밍과 실시간 대화 등 모든 걸 하기 전에 유저로부터 비디오를 가져와야함.
    그러기 위해 다음의 것들을 설정해줘야함.
    1. 먼저 유저로부터 비디오를 가져와 화면에 비디오를 보여줘야함.
    2. 버튼을 만들건데, 마이크를 음소거 및 해제 버튼이랑 캠을 on/off하는 버튼임.
    3. 휴대폰에서 사용하면 카메라 전/후면 체인지해서 좋을 것임.

1-1 User 비디오 가져오기

(frontend)

  • home.pug
    -> playsinline: 모바일 브라우저가 필요로 하는 property임. 원하지도 않는데, 자동으로 전체화면
    이 되는 것을 막아줄 것임. 모바일 비디오 플레이어로 재생되는 것이 아닌, 웹사이트에서 재생되도록
    하기 위한 코드임.
main
    video#myFace(autoplay,playsinline, width="400", height="400")
  • app.js
    Web API 모음
    getUserMedia MDN
    -> MediaDvices: 인터페이스의 getUserMedia() 메서드는 사용자에게 미디어 입력 장치 사용
    권한을 요청하며, 사용자가 수락하면 요청한 미디어 종류의 트랙을 포함한 MediaStream (en-
    US)을 반환합니다.
//MeadiaDivces.getUserMedia 예시
async function getMedia(constraints) {
  let stream = null;

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

-> srcObject: 인터페이스 의 srcObject속성은 에 HTMLMediaElement연결된
미디어의 소스 역할을 하는 개체를 설정하거나 반환합니다.

//srcObject 예시
const mediaStream = await navigator.mediaDevices.getUserMedia({video: true});
const video = document.createElement('video');
video.srcObject = mediaStream;

-> app.js코드

const myFace = document.getElementById("myFace");

let myStream;

async function getMedia(){
    try{
        myStream = await navigator.mediaDevices.getUserMedia({
            audio: true,
            video: true,
        });//user의 media를 가져옴(우린 특정 값을 줘서 카메라와 오디오를 가져옴)
        myFace.srcObject = myStream;//비디오를 Html에 넣어서 화면에 보이도록 함.
    } catch(error){
        console.log(error)
    }
};

getMedia();

1-2 음소거 및 카메라 on/off 버튼 만들기.

forntend

  • home.pug
main
    div#myStream
          video#myFace(autoplay,playsinline, width="400", height="400")
          button#mute Mute
          button#camera Turn Camera Off
  • app.js
    -> JS 조건식
    -> ! :부정연산자 - 값이 true면 false를, false면 true를 반환/ 또는 값이 있다면 실행 안하고 없다면
    실행함
    -> if (조건식): 조건이 참일 때.
    -> streamTrack : 우리가 만드는 stream은 track을 제공함. 비디오가 하나의 track이 되고, 오디
    오가 하나의 track이 될 수 있음. 그리고 자막 track도 가질 수 있음. 우리는 track에 접근할 수 있음.
    -> forEach() 메서드는 주어진 함수를 배열 요소 각각에 대해 실행합니다.
    -> myStream.getAudio(혹은 Video)Tracks().forEach((track) => (track.enabled = !track.enabled)): click이벤트 리스너 내에서 클릭이 있을 때마다, track.enable값을 반대되는 값으로 바꾸어 준다는 뜻임. "!"는 부정연산자 이므로, 값이 true면 false를, false면, true를 반환함.
//밑의 코드들 추가해주기
const muteBtn = document.getElementById("mute");
const cameraBtn = document.getElementById("camera");

let muted = false;
let cameraOff = false;

function handleMuteClick(){
    //console.log(myStream.getAudioTracks()); -> track정보(inspect) 제공
    myStream.getAudioTracks().forEach((track) => (track.enabled = !track.enabled));
    if(!muted){//muted = true라면 - default와 반대되는 값이라면// !: 반대되는 값을 리턴해줌
        muteBtn.innerText = "Unmute";
        muted = true;
    } else {
        muteBtn.innerText = "Mute";
        muted = false;
    }
}
function handleCameraClick(){
    //console.log(myStream.getVideoTracks()); -> track정보(inspect) 제공
    myStream.getVideoTracks().forEach((track) => (track.enabled = !track.enabled));
    if(cameraOff){//cameraOff가 참일 때
        cameraBtn.innerText = "Turn Camera Off";
        cameraOff = false;
    } else{
        cameraBtn.innerText = "Turn Camera On";
        cameraOff = true;
    }
}

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

1.3 유저가 사용할 Camera 선택하기

video Inputs가져오기

-> home.pug
-> select: 선택창 / option: select창의 내용물

 //추가
select#cameras

//최종 코드
main
    div#myStream
           video#myFace(autoplay,playsinline, width="400", height="400")
           button#mute Mute
           button#camera Turn Camera Off
           select#cameras 
                <!--내용물: option(value="device") Face Camera 이게 내용물이 될 것임-->

-> app.js

  • ~~ MediaDevices.eumerateDevices MDN MediaDevices의 enumerateDevices() 메서드는 사용(또는 접근)이 가능한 미디어 입력장치나 출력장치들의 리스트를 가져옵니다. 예를 들면 마이크, 카메라, 헤드셋 등의 미디어 입/출력 장치 리스트를 불러오는 것 이죠. 이 메서드는 Promise (en-US)를 반환하는데, 이 Promise가 resolve되면 장치(device)정보가 들어있는 MediaDeviceInfo (en-US) 배열(array)을 확인할 수 있습니다. ~~

  • filter((객체) => 조건문): array에다가 실행하는 함수임. array의 객체중 조건문에 해당하는 즉, 조건문에 true를 반환하는 애들만 따로 모아서 새 array를 만듦

  • ? : 조건부 연산자.

condition ? value1 : value2;

//condition이 truthy라면 value1이, 그렇지 않으면 value2가 반환됩니다.
    1. 여기서 우리는 MediaDevices.eumerateDevice를 이용해 장치들을 불러온 뒤, 카메라인 장치들만 모아서 새 array를 만들 것임.
    1. 새 array의 item들을 html의 select창에서 선택하면, 우리의 비디오 장치가 선택한 장치로 되도록 할 것임. 이 때 stream이 다시 시작됨.
const camerasSelect = document.getElementById("cameras");

async function getCameras(){
    try{
        const devices = await navigator.mediaDevices.enumerateDevices();
        //console.log(devices); -> 확인해보삼
        const cameras = devices.filter((device) => device.kind ===  "videoinput");//true인 애들만 뽑아서 새 array만듦.
        //console.log(cameras); -> videoinput만 있는 새 array 만들어짐.
      	const currentCamera = myStream.getVideoTracks()[0];//현재 사용중인 카메라 정보
        cameras.forEach((camera) => {
            const option = document.createElement("option");
            option.value = camera.deviceId;
            option.innerText = camera.label;
          	if(currentCamera.label === camera.label){
                option.selected = true;
            };//현재 카메라 정보가 카메라 목록에 있는 것과 이름이 같다면, 그 option이 select되게 해라. -> 처음 시작할 때, 현재 카메라가 선택되도록 하고 싶어서 그런 것임.
            camerasSelect.appendChild(option);//카메라 장치 목록을 select창에 넣음.
        })
    } catch(error){
        console.log(error);
    }
}

async function getMedia(deviceId){
  	const initialConstrains = {
        audio: true,
        video: { facingMode : "user" },
    };
    const cameraConstrains = {
        audio: true,
        video: { deviceId: { exact: deviceId } },//우리가 임의로 비디오를 바꿀 수 있도록 constrain을 설정해주는 것임.
    }
    try{
        myStream = await navigator.mediaDevices.getUserMedia(
        	deviceId? cameraConstrains : initialConstrains
        );//user의 media를 가져옴(이 때 특정 값은 ?(조건부 연산자)를 이용해 정해주고 특정 media객체를 가져옴)
        myFace.srcObject = myStream;
        if(!deviceId){
            await getCameras();
        }//내 장치에 있는 카메라들 가져오기! -> if문으로 계속 실행되는거 막기
    } catch(error){
        console.log(error)
    }
};

async function handleCameraChange(){
    await getMedia(camerasSelect.value);//select입력 값을 media시작 함수에 넣어주고 시작함수에서는 이를 받아들여, 카메라를 사용함.
}

camerasSelect.addEventListener("input", handleCameraChange);//select의 이벤트는 input임

User Video 만들기 done

-> 결과 코드

home.pug

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")
        link(rel="stylesheet", href="https://unpkg.com/mvp.css")
        title Noom
    body 
        header 
            h1 Noom 
        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") 

app.js

const frontSocket = io();//backend Socket과 연결됨.

const myFace = document.getElementById("myFace");
const muteBtn = document.getElementById("mute");
const cameraBtn = document.getElementById("camera");
const camerasSelect = document.getElementById("cameras");

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

async function getCameras(){
    try{
        const devices = await navigator.mediaDevices.enumerateDevices();
        //console.log(devices); -> 확인해보삼
        const cameras = devices.filter((device) => device.kind ===  "videoinput");//true인 애들만 뽑아서 새 array만듦.
        //console.log(cameras); -> videoinput만 있는 새 array 만들어짐.
        const currentCamera = myStream.getVideoTracks()[0];
        cameras.forEach((camera) => {
            const option = document.createElement("option");
            option.value = camera.deviceId;
            option.innerText = camera.label;
            if(currentCamera.label === camera.label){
                option.selected = true;
            }
            camerasSelect.appendChild(option);
        })
    } catch(error){
        console.log(error);
    }
}

async function getMedia(deviceId){
    const initialConstrains = {
        audio: true,
        video: { facingMode : "user" },
    };
    const cameraConstrains = {
        audio: true,
        video: { deviceId: { exact: deviceId } },
    }
    try{
        myStream = await navigator.mediaDevices.getUserMedia(
            deviceId? cameraConstrains : initialConstrains
        );//user의 media를 가져옴(우린 특정 값을 줘서 카메라와 오디오를 가져옴)
        myFace.srcObject = myStream;
        if(!deviceId){
            await getCameras();
        }// deviceId가 없다면 카메라 목록을 가져와라! -> if문 없이 하면 카메라 바꿀 때마다 이 gerMedia함수가 계속 실행되니까 목록이 늘어남
    } catch(error){
        console.log(error)
    }
};

getMedia();

function handleMuteClick(){
    //console.log(myStream.getAudioTracks()); -> track정보(inspect) 제공
    myStream.getAudioTracks().forEach((track) => (track.enabled = !track.enabled));
    if(!muted){//muted = true라면 - default와 반대되는 값이라면// !: 반대되는 값을 리턴해줌
        muteBtn.innerText = "Unmute";
        muted = true;
    } else {
        muteBtn.innerText = "Mute";
        muted = false;
    }
}
function handleCameraClick(){
    //console.log(myStream.getVideoTracks()); -> track정보(inspect) 제공
    myStream.getVideoTracks().forEach((track) => (track.enabled = !track.enabled));
    if(cameraOff){//cameraOff가 참일 때
        cameraBtn.innerText = "Turn Camera Off";
        cameraOff = false;
    } else{
        cameraBtn.innerText = "Turn Camera On";
        cameraOff = true;
    }
}

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

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

-> 다음 강의에선 상대방 비디오 가져오기 등등 할 것임.

profile
github: https://github.com/WKlee0607

0개의 댓글