이미지 로딩 중...

적응형 비트레이트 및 해상도 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 21. · 7 Views

적응형 비트레이트 및 해상도 완벽 가이드

WebRTC로 영상 통화를 만들 때 네트워크 상황에 따라 영상 품질이 자동으로 조절되는 기술을 배워봅니다. 비트레이트와 해상도를 동적으로 조정하여 끊김 없는 영상 통신을 제공하는 방법을 실무 중심으로 알아봅니다.


목차

  1. 동적_비트레이트_조정
  2. 해상도_자동_변경
  3. Simulcast_구현
  4. 네트워크_상태_감지
  5. 대역폭_제한_설정
  6. 품질_우선순위_관리

1. 동적_비트레이트_조정

시작하며

여러분이 영상 통화 앱을 만들다가 이런 상황을 겪어본 적 있나요? WiFi에서는 선명한 화질로 잘 보이다가, 사용자가 지하철로 이동하니 갑자기 영상이 뚝뚝 끊기고 화면이 멈춰버립니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 네트워크 대역폭은 시시각각 변하는데, 영상 품질은 그대로 고정되어 있어서 생기는 문제입니다.

사용자는 결국 통화를 포기하고, 앱에 대한 신뢰도가 떨어지게 됩니다. 바로 이럴 때 필요한 것이 동적 비트레이트 조정입니다.

마치 유튜브가 인터넷 속도에 따라 자동으로 화질을 조절하듯이, 실시간으로 네트워크 상황을 모니터링하고 비트레이트를 자동으로 올리거나 낮춰서 끊김 없는 영상 통화를 제공할 수 있습니다.

개요

간단히 말해서, 동적 비트레이트 조정은 네트워크 상황에 맞춰 영상 데이터 전송량을 자동으로 조절하는 기술입니다. 비트레이트란 1초당 전송되는 데이터의 양을 의미하는데, 높을수록 화질이 좋지만 그만큼 많은 대역폭이 필요합니다.

예를 들어, 회의실에서 고속 WiFi를 사용하다가 카페의 느린 공유기로 이동했을 때, 같은 비트레이트를 유지하면 영상이 버퍼링되거나 끊기게 됩니다. 기존에는 개발자가 고정된 비트레이트 값을 설정했다면, 이제는 WebRTC의 getStats API를 활용해서 실시간으로 패킷 손실률, RTT(왕복 시간), 사용 가능한 대역폭을 측정하고 자동으로 조절할 수 있습니다.

이 기술의 핵심 특징은 첫째, 실시간 네트워크 모니터링, 둘째, 자동 품질 조정, 셋째, 사용자 경험 최적화입니다. 이러한 특징들이 중요한 이유는 사용자가 네트워크 상황을 신경 쓰지 않고도 항상 최적의 통화 품질을 경험할 수 있기 때문입니다.

코드 예제

// WebRTC PeerConnection의 통계를 1초마다 확인하여 비트레이트 조정
const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');

async function adjustBitrate() {
  const stats = await sender.getStats();
  let bytesSent = 0;
  let packetsLost = 0;

  stats.forEach(report => {
    if (report.type === 'outbound-rtp' && report.kind === 'video') {
      bytesSent = report.bytesSent;
      packetsLost = report.packetsLost;
    }
  });

  // 패킷 손실률이 5% 이상이면 비트레이트 감소
  const lossRate = packetsLost / (packetsLost + report.packetsSent);
  if (lossRate > 0.05) {
    const params = sender.getParameters();
    params.encodings[0].maxBitrate = 500000; // 500kbps로 제한
    await sender.setParameters(params);
  }
}

setInterval(adjustBitrate, 1000); // 1초마다 체크

설명

이것이 하는 일: 이 코드는 WebRTC 연결의 품질을 실시간으로 모니터링하고, 네트워크 상황이 나빠지면 자동으로 비트레이트를 낮춰서 영상 끊김을 방지합니다. 첫 번째로, getSenders()로 비디오 전송자를 찾고 getStats()를 호출하여 현재 전송 상태를 확인합니다.

이 함수는 Promise를 반환하는데, 그 안에는 전송된 바이트 수, 손실된 패킷 수, RTT 등 다양한 통계 정보가 들어 있습니다. 왜 이렇게 하는지?

네트워크 문제를 조기에 감지하기 위해서입니다. 그 다음으로, stats 객체를 순회하면서 'outbound-rtp' 타입의 리포트를 찾습니다.

이 리포트에서 bytesSent(전송된 바이트)와 packetsLost(손실된 패킷)를 추출하여 패킷 손실률을 계산합니다. 만약 손실률이 5%를 넘으면 네트워크가 불안정하다고 판단합니다.

마지막으로, getParameters()로 현재 인코딩 설정을 가져온 후 maxBitrate를 500kbps로 낮춰서 setParameters()로 다시 적용합니다. 이렇게 하면 전송하는 데이터 양이 줄어들어 패킷 손실이 감소하고, 영상이 끊기지 않고 부드럽게 재생됩니다.

여러분이 이 코드를 사용하면 사용자가 WiFi에서 모바일 데이터로 전환할 때도 자동으로 화질이 조정되어 끊김 없는 통화를 유지할 수 있습니다. 또한 네트워크가 회복되면 다시 비트레이트를 올려서 고화질로 전환할 수도 있습니다.

실무에서는 단순히 비트레이트만 조절하는 것이 아니라, 프레임레이트(fps)와 해상도도 함께 조절하는 경우가 많습니다. 예를 들어, 비트레이트를 낮출 때 해상도도 720p에서 480p로 낮추면 더 효과적입니다.

실전 팁

💡 비트레이트를 갑자기 크게 변경하면 화질이 급변해서 사용자가 불편함을 느낄 수 있습니다. 점진적으로 10-20%씩 조정하는 것이 좋습니다.

💡 패킷 손실률만 보지 말고 RTT(왕복 시간)도 함께 확인하세요. RTT가 200ms 이상이면 네트워크 지연이 심각한 상태입니다.

💡 setParameters()를 너무 자주 호출하면 인코더가 불안정해질 수 있습니다. 최소 1초 간격으로 호출하고, 변경이 필요할 때만 호출하세요.

💡 모바일에서는 배터리 소모를 고려해야 합니다. 비트레이트를 낮추면 인코딩 부하도 줄어들어 배터리 수명이 늘어납니다.

💡 실제 프로덕션에서는 최소/최대 비트레이트 범위를 정해두세요. 예를 들어, 150kbps 미만으로는 내려가지 않도록 제한하면 화질이 너무 나빠지는 것을 방지할 수 있습니다.


2. 해상도_자동_변경

시작하며

여러분이 Zoom이나 Google Meet 같은 화상회의 앱을 사용할 때 이런 경험을 해보셨나요? 참가자가 많아지면 각 사람의 화면이 작아지면서 자연스럽게 화질도 낮아집니다.

이런 현상은 우연이 아닙니다. 개발자들이 의도적으로 해상도를 조정한 것인데, 같은 대역폭으로 더 많은 사람의 영상을 전송하기 위해서입니다.

만약 해상도를 고정하면 일부 사용자의 영상이 아예 표시되지 않거나 심하게 끊길 수 있습니다. 바로 이럴 때 필요한 것이 해상도 자동 변경입니다.

네트워크 상황, 화면 크기, 참가자 수 등을 고려해서 최적의 해상도를 선택하여 모든 사용자가 원활한 영상 통화를 즐길 수 있게 만듭니다.

개요

간단히 말해서, 해상도 자동 변경은 영상의 가로x세로 픽셀 수를 상황에 맞게 동적으로 조절하는 기술입니다. 해상도는 화질을 결정하는 가장 중요한 요소인데, 1080p(1920x1080)는 720p(1280x720)보다 4배 많은 데이터를 전송해야 합니다.

예를 들어, 화상회의에서 내 얼굴이 작은 썸네일로 표시될 때는 360p만으로도 충분하지만, 화면 공유를 받아볼 때는 최소 720p 이상이 필요합니다. 기존에는 getUserMedia()로 한 번 설정한 해상도를 계속 사용했다면, 이제는 track.applyConstraints()를 사용해서 실시간으로 변경할 수 있습니다.

이 기술의 핵심 특징은 첫째, 트랙을 다시 생성하지 않고 즉시 변경 가능, 둘째, 화면 크기에 맞춘 최적화, 셋째, 대역폭 효율성 향상입니다. 이러한 특징들이 중요한 이유는 불필요하게 높은 해상도로 전송하면 대역폭 낭비일 뿐만 아니라 다른 참가자들의 연결에도 악영향을 미치기 때문입니다.

코드 예제

// 비디오 트랙의 해상도를 실시간으로 변경하는 함수
const videoTrack = stream.getVideoTracks()[0];

async function changeResolution(width, height) {
  try {
    // 트랙을 다시 생성하지 않고 제약조건만 변경
    await videoTrack.applyConstraints({
      width: { ideal: width },
      height: { ideal: height },
      frameRate: { ideal: 30 }
    });
    console.log(`해상도 변경 완료: ${width}x${height}`);
  } catch (error) {
    console.error('해상도 변경 실패:', error);
  }
}

// 네트워크 상황에 따라 해상도 자동 조정
function autoAdjustResolution(bandwidth) {
  if (bandwidth > 2000000) { // 2Mbps 이상
    changeResolution(1280, 720); // 720p
  } else if (bandwidth > 1000000) { // 1Mbps 이상
    changeResolution(640, 480); // 480p
  } else {
    changeResolution(320, 240); // 240p
  }
}

설명

이것이 하는 일: 이 코드는 현재 사용 중인 비디오 트랙의 해상도를 끊김 없이 즉시 변경하여, 네트워크 상황에 최적화된 영상 품질을 제공합니다. 첫 번째로, getVideoTracks()[0]으로 미디어 스트림에서 비디오 트랙을 가져옵니다.

여기서 중요한 점은 트랙을 새로 만드는 것이 아니라 기존 트랙을 재활용한다는 것입니다. 왜 이렇게 하는지?

트랙을 새로 만들면 PeerConnection을 다시 연결해야 하는데, 이 과정에서 영상이 몇 초간 끊기기 때문입니다. 그 다음으로, applyConstraints()를 호출하여 width와 height 제약조건을 변경합니다.

여기서 'ideal' 키워드를 사용하는 것이 핵심인데, 이는 "가능하면 이 값을 사용하되, 안 되면 가장 가까운 값을 선택하라"는 의미입니다. 만약 'exact'를 사용하면 카메라가 해당 해상도를 지원하지 않을 때 에러가 발생합니다.

마지막으로, autoAdjustResolution() 함수가 현재 대역폭을 확인하고 적절한 해상도를 선택합니다. 2Mbps 이상이면 720p, 1-2Mbps면 480p, 그 이하면 240p로 설정합니다.

이렇게 하면 대역폭이 부족한 상황에서도 영상이 끊기지 않고 부드럽게 재생됩니다. 여러분이 이 코드를 사용하면 사용자가 전체화면으로 볼 때는 고해상도, 작은 창으로 볼 때는 저해상도로 자동 전환되어 불필요한 데이터 전송을 줄일 수 있습니다.

또한 화면 공유 중일 때는 카메라 해상도를 낮춰서 화면 공유 품질을 높이는 등 유연한 대응이 가능합니다. 실무에서는 화면 크기를 감지하는 ResizeObserver를 함께 사용합니다.

예를 들어, 비디오 엘리먼트의 크기가 320x240보다 작으면 자동으로 해상도를 낮추고, 크면 높이는 식입니다.

실전 팁

💡 해상도를 변경할 때는 항상 16:9 또는 4:3 같은 표준 종횡비를 유지하세요. 이상한 비율을 사용하면 영상이 왜곡됩니다.

💡 모바일 디바이스에서는 전면 카메라와 후면 카메라가 지원하는 해상도가 다릅니다. applyConstraints() 실패 시 대체 해상도 목록을 준비하세요.

💡 해상도를 올릴 때는 1-2초 정도 지연을 두세요. 네트워크가 일시적으로 좋아졌다가 다시 나빠질 수 있어서, 너무 빨리 올리면 곧바로 다시 낮춰야 합니다.

💡 frameRate도 함께 조절하면 더 효과적입니다. 저해상도일 때는 30fps, 고해상도일 때는 15-20fps로 설정하면 대역폭을 절약할 수 있습니다.

💡 화면 공유 시에는 해상도보다 프레임레이트를 낮추는 것이 낫습니다. 문서나 프레젠테이션은 움직임이 적어서 5-10fps만으로도 충분합니다.


3. Simulcast_구현

시작하며

여러분이 100명이 참여하는 온라인 세미나를 개최한다고 상상해보세요. 어떤 사람은 초고속 인터넷을 사용하고, 어떤 사람은 느린 모바일 데이터를 사용합니다.

모든 사람에게 같은 화질의 영상을 보내면 어떻게 될까요? 이런 문제는 대규모 화상회의에서 매우 심각합니다.

고화질로 보내면 느린 네트워크 사용자는 영상을 받을 수 없고, 저화질로 보내면 빠른 네트워크 사용자는 불만족스럽습니다. 게다가 서버에서 일일이 화질을 변환하려면 엄청난 컴퓨팅 파워가 필요합니다.

바로 이럴 때 필요한 것이 Simulcast입니다. 발신자가 동시에 여러 화질의 영상을 보내면, 각 수신자가 자신의 네트워크 상황에 맞는 화질을 선택해서 받을 수 있습니다.

마치 유튜브가 480p, 720p, 1080p를 동시에 제공하는 것처럼요.

개요

간단히 말해서, Simulcast는 하나의 영상을 여러 품질로 동시에 인코딩해서 전송하는 기술입니다. 이 기술이 필요한 이유는 SFU(Selective Forwarding Unit) 서버가 각 수신자에게 최적의 화질을 선택해서 보낼 수 있기 때문입니다.

예를 들어, 강연자 영상을 받는 100명 중에서 WiFi 사용자에게는 720p, 4G 사용자에게는 480p, 3G 사용자에게는 240p를 보낼 수 있습니다. 기존에는 서버가 받은 고화질 영상을 실시간으로 트랜스코딩(변환)해야 했다면, Simulcast를 사용하면 발신자의 CPU로 인코딩을 분산시켜서 서버 부하를 크게 줄일 수 있습니다.

이 기술의 핵심 특징은 첫째, 클라이언트 측 인코딩으로 서버 부하 감소, 둘째, 수신자별 맞춤 화질 제공, 셋째, 실시간 화질 전환 가능입니다. 이러한 특징들이 중요한 이유는 대규모 화상회의를 저렴한 비용으로 운영할 수 있고, 모든 사용자가 자신의 환경에 최적화된 경험을 얻을 수 있기 때문입니다.

코드 예제

// Simulcast를 활성화하여 3개 레이어(고/중/저 화질)로 전송
const sender = peerConnection.addTrack(videoTrack, stream);

// 인코딩 파라미터 설정: 3개의 다른 해상도/비트레이트
const encodings = [
  {
    rid: 'h', // high quality
    maxBitrate: 1200000, // 1.2Mbps
    scaleResolutionDownBy: 1.0 // 원본 해상도
  },
  {
    rid: 'm', // medium quality
    maxBitrate: 600000, // 600kbps
    scaleResolutionDownBy: 2.0 // 해상도 1/2
  },
  {
    rid: 'l', // low quality
    maxBitrate: 200000, // 200kbps
    scaleResolutionDownBy: 4.0 // 해상도 1/4
  }
];

// Simulcast 적용
await sender.setParameters({ encodings });

// 수신 측에서 원하는 레이어 선택
await receiver.getParameters().encodings[0].active = true; // high 레이어만 활성화

설명

이것이 하는 일: 이 코드는 단일 비디오 트랙을 3개의 서로 다른 품질 레이어로 인코딩하여 동시에 전송하고, 수신자가 네트워크 상황에 맞는 레이어를 선택할 수 있게 합니다. 첫 번째로, addTrack()으로 비디오 트랙을 PeerConnection에 추가하면 RTCRtpSender 객체가 반환됩니다.

이 객체를 통해 인코딩 파라미터를 설정할 수 있는데, 여기서 'rid'는 각 레이어를 구분하는 고유 ID입니다. 'h', 'm', 'l'은 관례적으로 high, medium, low를 의미합니다.

그 다음으로, 각 레이어의 maxBitrate와 scaleResolutionDownBy를 설정합니다. scaleResolutionDownBy가 핵심인데, 2.0이면 가로세로를 각각 절반으로 줄인다는 의미입니다.

예를 들어 원본이 1280x720이면, 중간 레이어는 640x360, 낮은 레이어는 320x180이 됩니다. 이렇게 하면 하나의 카메라 소스로 3개의 서로 다른 화질을 만들어냅니다.

마지막으로, setParameters()로 인코딩 설정을 적용하면 브라우저가 자동으로 3개의 인코더를 실행합니다. 수신 측에서는 getParameters()로 사용 가능한 레이어를 확인하고, active 속성을 true/false로 설정하여 원하는 레이어만 받을 수 있습니다.

여러분이 이 코드를 사용하면 SFU 서버는 각 수신자의 대역폭을 확인하여 자동으로 적절한 레이어를 선택해서 포워딩할 수 있습니다. 예를 들어, 참가자 A가 갑자기 WiFi에서 LTE로 전환하면 서버는 즉시 high 레이어에서 medium 레이어로 전환하여 끊김 없는 시청 경험을 제공합니다.

실무에서는 Simulcast를 켜면 인코딩 부하가 크게 증가하므로, 모바일 디바이스에서는 2개 레이어만 사용하거나, 발표자 모드일 때만 활성화하는 등의 최적화가 필요합니다. 또한 VP9이나 AV1 코덱은 SVC(Scalable Video Coding)를 지원하여 Simulcast보다 효율적일 수 있습니다.

실전 팁

💡 Simulcast는 발신자의 CPU 사용량을 2-3배 증가시킵니다. 배터리가 부족한 모바일 기기에서는 선택적으로 비활성화하세요.

💡 모든 브라우저가 Simulcast를 지원하는 것은 아닙니다. Safari는 제한적으로만 지원하므로, 기능 감지 후 폴백 로직을 준비하세요.

💡 레이어 개수는 3개가 적당합니다. 그 이상 늘리면 인코딩 효율이 떨어지고 CPU만 낭비됩니다.

💡 scaleResolutionDownBy는 2의 배수(1.0, 2.0, 4.0)를 사용하세요. 1.5 같은 값은 픽셀 보간 오버헤드가 발생합니다.

💡 화면 공유에는 Simulcast를 사용하지 마세요. 텍스트는 해상도를 낮추면 읽을 수 없게 되므로, 단일 고화질 레이어만 전송하는 것이 좋습니다.


4. 네트워크_상태_감지

시작하며

여러분이 영상 통화 중에 갑자기 화질이 떨어졌다고 상상해보세요. 사용자는 "내 인터넷이 느린가?" 하고 의아해하지만, 실제로는 상대방의 네트워크가 문제일 수도 있고, 중간 경로의 라우터가 혼잡할 수도 있습니다.

이런 문제는 실시간 통신에서 매우 흔합니다. 네트워크는 시시각각 변하는데, 개발자가 이를 정확히 파악하지 못하면 엉뚱한 곳에서 해결책을 찾게 됩니다.

사용자는 결국 "앱이 불안정하다"고 느끼고 경쟁사 앱으로 떠나갑니다. 바로 이럴 때 필요한 것이 네트워크 상태 감지입니다.

WebRTC의 getStats API를 활용하면 패킷 손실률, RTT, jitter, 대역폭 등을 실시간으로 모니터링하여 문제를 조기에 발견하고 자동으로 대응할 수 있습니다.

개요

간단히 말해서, 네트워크 상태 감지는 WebRTC 연결의 품질 지표를 실시간으로 수집하고 분석하는 기술입니다. 이 기술이 필요한 이유는 단순히 인터넷 속도만으로는 통화 품질을 예측할 수 없기 때문입니다.

예를 들어, 100Mbps 인터넷을 사용해도 패킷 손실률이 10%라면 영상이 심하게 끊기고, 반대로 10Mbps여도 손실률이 0%라면 부드러운 통화가 가능합니다. 기존에는 사용자가 "영상이 끊겨요"라고 신고하기 전까지 문제를 모르고 지나갔다면, 이제는 getStats()로 지표를 모니터링하여 문제가 발생하기 전에 미리 조치를 취할 수 있습니다.

이 기술의 핵심 특징은 첫째, 실시간 품질 지표 수집, 둘째, 문제의 근본 원인 파악, 셋째, 자동화된 품질 개선 조치입니다. 이러한 특징들이 중요한 이유는 개발자가 네트워크 상태를 정확히 이해해야 적절한 최적화 전략을 선택할 수 있고, 사용자에게 투명한 정보를 제공할 수 있기 때문입니다.

코드 예제

// 네트워크 품질 지표를 실시간으로 모니터링
class NetworkMonitor {
  constructor(peerConnection) {
    this.pc = peerConnection;
    this.previousStats = new Map();
  }

  async getNetworkQuality() {
    const stats = await this.pc.getStats();
    const metrics = {
      packetLoss: 0,
      rtt: 0,
      jitter: 0,
      bandwidth: 0
    };

    stats.forEach(report => {
      if (report.type === 'inbound-rtp' && report.kind === 'video') {
        // 패킷 손실률 계산
        const packetsLost = report.packetsLost || 0;
        const packetsReceived = report.packetsReceived || 0;
        metrics.packetLoss = packetsLost / (packetsLost + packetsReceived) * 100;

        // Jitter (패킷 도착 시간의 변동성)
        metrics.jitter = report.jitter * 1000; // 초를 ms로 변환
      }

      if (report.type === 'candidate-pair' && report.state === 'succeeded') {
        // RTT (Round Trip Time)
        metrics.rtt = report.currentRoundTripTime * 1000; // 초를 ms로 변환

        // 사용 가능한 대역폭 추정
        metrics.bandwidth = report.availableOutgoingBitrate || 0;
      }
    });

    return metrics;
  }

  // 네트워크 상태 평가
  evaluateQuality(metrics) {
    if (metrics.packetLoss > 5 || metrics.rtt > 300 || metrics.jitter > 50) {
      return 'poor'; // 나쁨
    } else if (metrics.packetLoss > 2 || metrics.rtt > 150) {
      return 'fair'; // 보통
    }
    return 'good'; // 좋음
  }
}

// 사용 예시
const monitor = new NetworkMonitor(peerConnection);
const metrics = await monitor.getNetworkQuality();
console.log(`품질: ${monitor.evaluateQuality(metrics)}, RTT: ${metrics.rtt}ms`);

설명

이것이 하는 일: 이 코드는 WebRTC 연결의 다양한 통계 정보를 수집하고 분석하여, 현재 네트워크 상태가 영상 통화에 적합한지 판단합니다. 첫 번째로, getStats()를 호출하면 RTCStatsReport 객체가 반환되는데, 이 안에는 수십 가지 리포트가 들어있습니다.

우리는 그 중에서 'inbound-rtp' 타입(받는 쪽 통계)과 'candidate-pair' 타입(연결 경로 통계)을 찾아야 합니다. 왜 이 두 가지인지?

inbound-rtp는 수신 품질을, candidate-pair는 네트워크 경로 품질을 나타내기 때문입니다. 그 다음으로, packetsLost와 packetsReceived를 사용하여 패킷 손실률을 계산합니다.

패킷 손실이 발생하면 영상의 일부 프레임이 깨지거나 사라지므로, 이 값이 5%를 넘으면 시청 경험이 크게 나빠집니다. jitter는 패킷이 도착하는 시간 간격의 변동성을 의미하는데, 높으면 영상이 부드럽지 않고 뚝뚝 끊기는 느낌을 줍니다.

마지막으로, currentRoundTripTime(RTT)은 패킷이 왕복하는데 걸리는 시간입니다. 300ms 이상이면 사용자가 지연을 체감하게 되고, 특히 양방향 대화에서 답답함을 느낍니다.

availableOutgoingBitrate는 현재 사용 가능한 전송 대역폭을 추정한 값으로, 이를 기반으로 비트레이트를 조절할 수 있습니다. 여러분이 이 코드를 사용하면 사용자에게 "현재 네트워크 상태가 좋지 않습니다"라는 알림을 표시하거나, 자동으로 화질을 낮추거나, 심지어 오디오 전용 모드로 전환하는 등 다양한 대응이 가능합니다.

또한 이 데이터를 서버에 전송하여 통화 품질을 분석하고 개선점을 찾을 수도 있습니다. 실무에서는 이러한 메트릭을 1초마다 수집하여 시계열 그래프로 시각화하면, 특정 시간대에 품질이 떨어지는 패턴을 발견할 수 있습니다.

예를 들어, 매일 오후 6시에 패킷 손실률이 급증한다면 사용자의 공유기 문제일 가능성이 높습니다.

실전 팁

💡 getStats()는 비용이 큰 작업이므로 1초 이하 간격으로 호출하지 마세요. 0.5초마다 호출하면 CPU 사용량이 급증합니다.

💡 패킷 손실률은 일시적으로 튈 수 있으므로, 5초 이동 평균을 사용하여 판단하세요. 단발성 스파이크는 무시해도 됩니다.

💡 모바일에서는 네트워크 타입(WiFi/4G/5G)도 함께 확인하세요. navigator.connection.effectiveType으로 얻을 수 있습니다.

💡 RTT가 갑자기 10배 이상 증가하면 네트워크 경로가 변경된 것일 수 있습니다. ICE restart를 시도하면 더 나은 경로를 찾을 수 있습니다.

💡 품질 지표를 사용자에게 보여줄 때는 숫자 대신 색상(녹색/노란색/빨간색) 또는 신호 막대로 표시하세요. 일반 사용자는 RTT가 무엇인지 모릅니다.


5. 대역폭_제한_설정

시작하며

여러분이 집에서 화상회의를 하면서 동시에 대용량 파일을 다운로드한다고 상상해보세요. 갑자기 화상회의 영상이 끊기고 음성이 로봇처럼 들립니다.

왜 그럴까요? 다운로드가 모든 대역폭을 차지해버렸기 때문입니다.

이런 문제는 가정뿐만 아니라 기업 환경에서도 흔합니다. 특히 여러 명이 같은 네트워크를 공유하는 사무실에서는 한 사람의 대용량 업로드가 다른 사람들의 화상회의를 망칠 수 있습니다.

개발자 입장에서는 "내 앱은 잘 만들었는데 왜 느리지?"라고 생각하지만, 실제로는 다른 앱들과의 대역폭 경쟁 문제입니다. 바로 이럴 때 필요한 것이 대역폭 제한 설정입니다.

WebRTC에서 maxBitrate를 설정하면 영상 통화가 사용할 수 있는 최대 대역폭을 제한하여, 다른 앱들과 네트워크를 공정하게 나눠 쓸 수 있습니다.

개요

간단히 말해서, 대역폭 제한 설정은 WebRTC 연결이 사용할 수 있는 최대 비트레이트를 지정하여 네트워크 자원을 효율적으로 관리하는 기술입니다. 이 기술이 필요한 이유는 무제한으로 대역폭을 사용하면 다른 중요한 통신(예: VoIP 전화, 화면 공유)이 방해받을 수 있기 때문입니다.

예를 들어, 화상회의 앱이 10Mbps를 쓰고 있는데 사용자의 업로드 속도가 10Mbps밖에 안 된다면, 웹 브라우징조차 불가능해집니다. 기존에는 브라우저가 자동으로 대역폭을 조절했지만 예측이 어렵고 때로는 과도하게 사용했다면, 이제는 setParameters()로 명시적으로 상한선을 정할 수 있습니다.

이 기술의 핵심 특징은 첫째, 예측 가능한 대역폭 사용, 둘째, 다중 스트림 환경에서의 공정성, 셋째, 배터리 및 데이터 절약입니다. 이러한 특징들이 중요한 이유는 사용자가 여러 앱을 동시에 사용하는 환경에서 WebRTC 앱이 독점적으로 동작하지 않고, 전체 시스템과 조화롭게 작동해야 하기 때문입니다.

코드 예제

// 비디오와 오디오에 각각 대역폭 제한 설정
async function setBandwidthLimit(peerConnection, maxVideoBitrate, maxAudioBitrate) {
  const senders = peerConnection.getSenders();

  for (const sender of senders) {
    if (!sender.track) continue;

    const params = sender.getParameters();

    if (sender.track.kind === 'video') {
      // 비디오는 여러 인코딩 레이어가 있을 수 있음
      if (!params.encodings || params.encodings.length === 0) {
        params.encodings = [{}];
      }

      // 각 레이어에 maxBitrate 설정 (bps 단위)
      params.encodings.forEach(encoding => {
        encoding.maxBitrate = maxVideoBitrate; // 예: 1000000 (1Mbps)
      });
    } else if (sender.track.kind === 'audio') {
      // 오디오는 보통 단일 인코딩
      if (!params.encodings || params.encodings.length === 0) {
        params.encodings = [{}];
      }
      params.encodings[0].maxBitrate = maxAudioBitrate; // 예: 64000 (64kbps)
    }

    await sender.setParameters(params);
  }
}

// 사용 예시: 비디오 1Mbps, 오디오 64kbps 제한
await setBandwidthLimit(peerConnection, 1000000, 64000);

// 모바일 데이터 사용 시 더 낮게 제한
if (navigator.connection && navigator.connection.type === 'cellular') {
  await setBandwidthLimit(peerConnection, 500000, 32000);
}

설명

이것이 하는 일: 이 코드는 비디오와 오디오 각각에 대역폭 상한을 설정하여, 네트워크가 혼잡하거나 제한된 환경에서도 안정적인 통화 품질을 보장합니다. 첫 번째로, getSenders()로 모든 미디어 전송자를 가져온 후 track.kind를 확인하여 비디오인지 오디오인지 구분합니다.

이렇게 구분하는 이유는 비디오와 오디오는 필요한 대역폭이 크게 다르기 때문입니다. 비디오는 보통 500kbps2Mbps가 필요하지만, 오디오는 32128kbps면 충분합니다.

그 다음으로, getParameters()로 현재 인코딩 파라미터를 가져옵니다. 여기서 주의할 점은 encodings 배열이 비어있을 수 있다는 것입니다.

이 경우 빈 객체를 하나 추가해야 합니다. 그런 다음 각 인코딩 레이어에 maxBitrate를 설정하는데, 단위는 bps(bits per second)입니다.

1Mbps는 1,000,000으로 표현합니다. 마지막으로, setParameters()로 변경사항을 적용합니다.

이 함수는 비동기이므로 await를 사용해야 합니다. 적용 후에는 브라우저가 자동으로 인코더를 조정하여, 아무리 네트워크가 좋아도 설정한 값 이상으로는 전송하지 않습니다.

여러분이 이 코드를 사용하면 사용자가 모바일 데이터를 쓸 때 자동으로 비트레이트를 낮춰서 데이터 요금을 절약할 수 있습니다. 또한 여러 참가자가 동시에 발언하는 회의에서 각 사람의 대역폭을 제한하여 전체 품질을 균등하게 유지할 수 있습니다.

실무에서는 네트워크 타입뿐만 아니라 배터리 잔량도 확인합니다. 배터리가 20% 미만이면 비트레이트를 낮춰서 인코딩 부하를 줄이고 배터리 수명을 연장할 수 있습니다.

또한 화면 공유 중일 때는 카메라 비트레이트를 크게 낮춰서 화면 공유에 대역폭을 집중시킬 수 있습니다.

실전 팁

💡 maxBitrate를 너무 낮게 설정하면 화질이 심하게 떨어집니다. 비디오는 최소 300kbps, 오디오는 최소 24kbps를 유지하세요.

💡 Simulcast 사용 시 각 레이어마다 다른 maxBitrate를 설정해야 합니다. 고화질 레이어는 1.5Mbps, 중간은 700kbps, 저화질은 300kbps 식으로요.

💡 setParameters()를 연속으로 여러 번 호출하면 이전 호출이 무시될 수 있습니다. 항상 getParameters() -> 수정 -> setParameters() 순서를 지키세요.

💡 모바일 데이터 요금이 비싼 국가의 사용자를 위해 "데이터 절약 모드"를 제공하세요. 이 모드에서는 비디오를 200kbps로 제한합니다.

💡 오디오 비트레이트는 코덱에 따라 다릅니다. Opus는 32kbps로도 괜찮지만, PCMU/PCMA는 최소 64kbps가 필요합니다.


6. 품질_우선순위_관리

시작하며

여러분이 9명이 참여하는 화상회의에서 발표를 하고 있다고 상상해보세요. 화면에는 발표자인 여러분이 크게 보이고, 나머지 8명은 작은 썸네일로 보입니다.

그런데 모든 사람의 영상을 똑같은 화질로 받는다면 어떨까요? 이런 상황은 대역폭과 CPU를 낭비하는 전형적인 예입니다.

작은 썸네일은 저해상도로도 충분한데, 고화질로 받으면 디코딩에만 CPU를 소모하고 실제로는 화질 차이도 느끼지 못합니다. 게다가 네트워크가 느리면 정작 중요한 발표자 영상이 끊길 수 있습니다.

바로 이럴 때 필요한 것이 품질 우선순위 관리입니다. 화면에 크게 보이는 영상은 고화질로, 작게 보이는 영상은 저화질로 받아서 제한된 자원을 효율적으로 사용할 수 있습니다.

개요

간단히 말해서, 품질 우선순위 관리는 각 영상 스트림의 중요도에 따라 서로 다른 화질을 요청하여 자원을 최적화하는 기술입니다. 이 기술이 필요한 이유는 사람의 시각적 주의는 제한적이기 때문입니다.

예를 들어, 갤러리 뷰에서 16명의 영상을 동시에 볼 때, 사용자는 실제로 2-3명에게만 집중합니다. 나머지는 단지 "누가 참석했는지" 확인하는 용도이므로, 낮은 화질로도 충분합니다.

기존에는 모든 영상을 같은 화질로 받았다면, 이제는 화면 크기, 레이아웃, 사용자 포커스에 따라 동적으로 각 스트림의 maxBitrate와 scaleResolutionDownBy를 조절할 수 있습니다. 이 기술의 핵심 특징은 첫째, 시각적 중요도 기반 자원 할당, 둘째, 동적 우선순위 변경, 셋째, 전체 대역폭 최적화입니다.

이러한 특징들이 중요한 이유는 대규모 회의에서 제한된 대역폭과 CPU로 모든 참가자를 표시하려면, 중요한 영상에 자원을 집중해야 하기 때문입니다.

코드 예제

// 영상 스트림에 우선순위를 설정하는 클래스
class VideoQualityManager {
  constructor() {
    this.receivers = new Map(); // trackId -> RTCRtpReceiver
  }

  // 우선순위에 따라 수신 품질 설정
  async setPriority(trackId, priority) {
    const receiver = this.receivers.get(trackId);
    if (!receiver) return;

    const params = receiver.getParameters();

    switch(priority) {
      case 'high': // 전체화면 또는 주 발표자
        params.encodings = [
          { rid: 'h', active: true },   // 고화질 활성화
          { rid: 'm', active: false },
          { rid: 'l', active: false }
        ];
        break;

      case 'medium': // 부화면
        params.encodings = [
          { rid: 'h', active: false },
          { rid: 'm', active: true },   // 중화질 활성화
          { rid: 'l', active: false }
        ];
        break;

      case 'low': // 썸네일
        params.encodings = [
          { rid: 'h', active: false },
          { rid: 'm', active: false },
          { rid: 'l', active: true }    // 저화질 활성화
        ];
        break;

      case 'off': // 화면에 보이지 않음
        params.encodings = [
          { rid: 'h', active: false },
          { rid: 'm', active: false },
          { rid: 'l', active: false }   // 모두 비활성화
        ];
        break;
    }

    await receiver.setParameters(params);
  }

  // 화면 레이아웃 변경 시 일괄 우선순위 조정
  async updateLayout(activeTrackId, visibleTrackIds) {
    // 주 발표자는 고화질
    await this.setPriority(activeTrackId, 'high');

    // 보이는 참가자는 중화질
    for (const trackId of visibleTrackIds) {
      if (trackId !== activeTrackId) {
        await this.setPriority(trackId, 'medium');
      }
    }

    // 나머지는 저화질 또는 비활성화
    for (const [trackId] of this.receivers) {
      if (trackId !== activeTrackId && !visibleTrackIds.includes(trackId)) {
        await this.setPriority(trackId, 'off');
      }
    }
  }
}

// 사용 예시
const qm = new VideoQualityManager();
await qm.updateLayout('speaker-123', ['participant-1', 'participant-2']);

설명

이것이 하는 일: 이 코드는 화상회의의 레이아웃과 사용자의 포커스에 맞춰 각 영상 스트림의 수신 품질을 자동으로 조절하여, 중요한 영상에 대역폭과 CPU를 집중시킵니다. 첫 번째로, Map 자료구조로 각 트랙 ID와 수신자(RTCRtpReceiver)를 매핑합니다.

화상회의에서는 여러 참가자의 영상을 동시에 받기 때문에, 각각을 구분하여 관리해야 합니다. setPriority() 함수는 'high', 'medium', 'low', 'off' 네 가지 우선순위를 지원하는데, 이는 전체화면, 부화면, 썸네일, 화면 밖 상태에 대응됩니다.

그 다음으로, Simulcast의 rid(레이어 ID)를 사용하여 각 우선순위에 맞는 레이어를 활성화합니다. 예를 들어, 'high' 우선순위는 'h' 레이어(고화질)만 활성화하고 나머지는 비활성화합니다.

이렇게 하면 발신자는 3개 레이어를 모두 보내지만, 수신자는 필요한 레이어만 선택적으로 받아서 대역폭을 절약합니다. 마지막으로, updateLayout() 함수가 레이아웃 변경을 감지하면 모든 트랙의 우선순위를 일괄 재조정합니다.

예를 들어, 사용자가 갤러리 뷰에서 발표자 뷰로 전환하면, 발표자는 즉시 고화질로 전환되고 나머지는 저화질 또는 비활성화됩니다. 이 과정은 1초 이내에 완료되어 사용자는 거의 즉각적인 화질 변화를 경험합니다.

여러분이 이 코드를 사용하면 16명 회의에서 모든 영상을 고화질로 받을 때 필요한 대역폭(예: 20Mbps)을 1/3 수준(예: 7Mbps)으로 줄일 수 있습니다. 또한 디코딩 CPU 사용량도 크게 감소하여 노트북 팬 소음이 줄어들고 배터리 수명이 늘어납니다.

실무에서는 IntersectionObserver API를 사용하여 실제로 화면에 보이는 비디오 엘리먼트만 고화질로 전환합니다. 사용자가 스크롤해서 특정 참가자가 화면 밖으로 나가면 자동으로 'off'로 설정하여 완전히 수신을 중단할 수 있습니다.

또한 네트워크가 느릴 때는 모든 우선순위를 한 단계씩 낮춰서(high->medium, medium->low) 전체적인 안정성을 확보할 수도 있습니다.

실전 팁

💡 우선순위를 변경할 때는 디바운싱(debouncing)을 적용하세요. 사용자가 빠르게 스크롤하면 불필요한 전환이 여러 번 발생할 수 있습니다.

💡 모바일에서는 화면이 작으므로 'high' 우선순위도 720p가 아닌 480p로 충분합니다. 디바이스 종류에 따라 최대 품질을 다르게 설정하세요.

💡 'off' 상태의 영상도 1-2초마다 한 프레임씩 받아서 정지 화면을 업데이트하면, 사용자가 "연결이 끊겼나?" 하는 걱정을 하지 않습니다.

💡 발표자가 바뀔 때는 이전 발표자를 'medium'으로 유지하세요. 즉시 'low'로 내리면 화질이 급격히 떨어져서 사용자가 불편함을 느낍니다.

💡 화면 공유가 시작되면 모든 카메라 영상을 'low' 또는 'off'로 전환하고, 화면 공유에 대역폭을 집중시키세요. 공유 화면은 텍스트가 많아서 고화질이 필수입니다.


#WebRTC#AdaptiveBitrate#Simulcast#NetworkOptimization#VideoStreaming#WebRTC,최적화

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

Docker 배포와 CI/CD 완벽 가이드

Docker를 활용한 컨테이너 배포부터 GitHub Actions를 이용한 자동화 파이프라인까지, 초급 개발자도 쉽게 따라할 수 있는 실전 배포 가이드입니다. AWS EC2에 애플리케이션을 배포하고 SSL 인증서까지 적용하는 전 과정을 다룹니다.

보안 강화 및 테스트 완벽 가이드

웹 애플리케이션의 보안 취약점을 방어하고 안정적인 서비스를 제공하기 위한 실전 보안 기법과 테스트 전략을 다룹니다. XSS, CSRF부터 DDoS 방어, Rate Limiting까지 실무에서 바로 적용 가능한 보안 솔루션을 제공합니다.

Redis 캐싱과 Socket.io 클러스터링 완벽 가이드

실시간 채팅 서비스의 성능을 획기적으로 향상시키는 Redis 캐싱 전략과 Socket.io 클러스터링 방법을 배워봅니다. 다중 서버 환경에서도 안정적으로 작동하는 실시간 애플리케이션을 구축하는 방법을 단계별로 알아봅니다.

반응형 디자인 및 UX 최적화 완벽 가이드

모바일부터 데스크톱까지 완벽하게 대응하는 반응형 웹 디자인과 사용자 경험을 개선하는 실전 기법을 학습합니다. Tailwind CSS를 활용한 빠른 개발부터 다크모드, 무한 스크롤, 스켈레톤 로딩까지 최신 UX 패턴을 실무에 바로 적용할 수 있습니다.

React 채팅 UI 구현 완벽 가이드

실시간 채팅 애플리케이션의 UI를 React로 구현하는 방법을 다룹니다. Socket.io 연동부터 컴포넌트 설계, 상태 관리까지 실무에 바로 적용할 수 있는 내용을 담았습니다.