프리뷰 | 네트워크 기반 동적 품질 조절 기능 개발 3편 Thumbnail

applyConstraints

13 min read

이전 글에서는 네트워크 기반 동적 품질 조절 기능에서 필수적이고 핵심인 네트워크 지표를 측정하고 현재 유저의 네트워크 상태가 좋은지 나쁜지를 판별하는 기능을 구현했다.

프리뷰 | 네트워크 기반 동적 품질 조절 기능 개발 3편

이번에는 마지막을 장식하는 WebRTC를 사용하기 위해 유저의 미디어 장치를 가져올 때 현재 네트워크 품질에 맞는 프리셋을 사용하도록 하는 기능을 만들었다.

이번 글에서도 어떻게 구현했는지에 대해서 적고 마지막에 테스트 영상으로 마무리해보도록 하겠다.

미디어 품질 프리셋

미디어 품질 프리셋은 네트워크 상태에 따라 동적으로 조절되어야하므로 사용자의 네트워크 품질이 낮아지면 프리셋도 함께 low한 프리셋으로 조절되어야 한다.

그렇기 때문에 아래와 같이 비디오의 해상도, 초당 프레임 레이트, 비트레이트를 설정해주었다. (아래 코드 블럭은 일부)

interface IQualityPreset {
  quality: "ultra" | "high" | "medium" | "low" | "very-low";
  video: {
    width: number;
    height: number;
    frameRate: number;
    videoBitrate: number;
  };
}


  ultra: {
    quality: "ultra",
    video: {
      width: 1280,
      height: 720,
      frameRate: 60,
      videoBitrate: 2500,
    },
  },
  high: {
    quality: "high",
    video: {
      width: 854,
      height: 480,
      frameRate: 30,
      videoBitrate: 1000,
    },
  },

어떻게 적용하나?

사용자의 미디어 장치를 가져오는 역할은 useMediaDevices 커스텀 훅에서 담당하고 있다. 사용자의 현재 네트워크 품질은 전역상태로 관리하고 있기 때문에 useMediaDevices 훅에서도 쉽게 접근할 수 있다.

유저의 미디어 장치로 부터 스트림을 얻어오는 함수의 일부분을 보자. preset.video.frameRate 이런 식으로 constraints가 적용된 것을 볼 수 있다.

// ...중략...
 // 현재의 네트워크 품질 가져오기
      const quality =
        useNetworkStore.getState().currentNetworkQuality || "medium";
      const preset = QualityPreset[quality];

      // 비디오와 오디오 스트림을 따로 가져오기
      let videoStream = null;
      let audioStream = null;

      try {
        videoStream = isVideoOn
          ? await navigator.mediaDevices.getUserMedia({
              video: selectedVideoDeviceId
                ? {
                    deviceId: selectedVideoDeviceId,
                    width: { ideal: preset.video.width },
                    height: { ideal: preset.video.height },
                    frameRate: {
                      ideal: preset.video.frameRate,
                      max: preset.video.frameRate + 5,
                    },
                  }
                : {
                    width: { ideal: preset.video.width },
                    height: { ideal: preset.video.height },
                    frameRate: {
                      ideal: preset.video.frameRate,
                      max: preset.video.frameRate + 5,
                    },
                  },
              audio: false,
            })
          : null;
      } catch (videoError) {
        console.warn("비디오 스트림을 가져오는데 실패했습니다:", videoError);
        setIsVideoOn(false);
      }

ideal 프로퍼티는 무엇인가?

ideal은 사전적인 의미로 이상적인 이라는 뜻을 가지고 있다. 즉, 가능하다면 이 수치로 맞춰주세요~와 비슷한 의미이다.

필요한 이유?

ideal 대신 고정 값을 사용하기 위한 exact 라는 항목이 존재한다. 하지만 만약 exact로 1920 x 1080 의 FHD 해상도를 지정한다면 어떻게 될까? 유저의 미디어 장치가 FHD 화질을 지원한다면 해당 해상도로 출력이 가능하다.

그렇다면 지원하지 않는 장치일 경우? 해본 결과 OverconstrainedError 라는 에러를 반환한다.

동적으로 조절하기

위 함수만 수정하면 초반에 얻어오는 스트림에만 적용되기 때문에 useEffect를 사용해서 네트워크 품질이 바뀔 때마다 새롭게 적용해주어야한다.

아래 코드는 useNetworkStore에서 현재 네트워크 품질 프리셋 정보 (ex: ultra)를 가져온다. 이후 사전 정의된 프리셋을 가져오고 stream 객체의 video 트랙에 applyConstarints 메서드를 활용해 적용해준다.

useEffect(() => {
    // 현재 네트워크 품질이 변경될 때마다 비디오 품질 조정하는 이펙트
    const currentQuality = useNetworkStore.getState().currentNetworkQuality;
    if (currentQuality && streamRef.current) {
      const videoTrack = streamRef.current.getVideoTracks()[0];
      if (videoTrack) {
        const preset = QualityPreset[currentQuality];

        videoTrack
          .applyConstraints({
            width: { ideal: preset.video.width },
            height: { ideal: preset.video.height },
            frameRate: {
              ideal: preset.video.frameRate,
              max: preset.video.frameRate + 5,
            },
          })
          .catch((error) => {
            console.warn("비디오 제약 조건 적용 실패:", error);
          });

        // // WebRTC 비트레이트 설정 (피어 연결이 있는 경우)
        if (peerConnections.current) {
          for (const peerConnection of Object.values(peerConnections.current)) {
            const sender = peerConnection
              .getSenders()
              .find((s) => s.track?.kind === "video");
            if (sender) {
              const parameters = sender.getParameters();
              if (!parameters.encodings) {
                parameters.encodings = [{}];
              }
              parameters.encodings[0].maxBitrate =
                preset.video.videoBitrate * 1000;
              sender.setParameters(parameters).catch((error) => {
                console.warn("비디오 비트레이트 설정 실패:", error);
              });
            }
          }
        }

        console.log(
          `네트워크 품질 변경: ${currentQuality}, 비디오 설정 업데이트됨`
        );
      }
    }
  }, [useNetworkStore.getState().currentNetworkQuality]);

applyConstraints가 있는지 모르고 처음에는 설마 품질 바뀔 때마다 새롭게 트랙 가져와야하나? 라는 절망에 빠질 뻔 했으나 다행히 금방 해당 메서드를 발견했다!!

이제 전역 네트워크 품질 상태를 구독하는 useEffect가 만들어졌다. 네트워크 품질이 달라지면 실행되어 품질을 조절해준다.

테스트

테스트에 앞서 내 웹캠은 해상도도 낮고 최대 프레임도 30프레임이라 적용되는지를 확인하기 위해 medium 해상도의 비율을 다르게 설정해서 테스트했다.

테스트 영상은 링크를 통해 볼 수 있다. (youtube 임베드 기능을 만들어야겠다..) 자막이랑 확대 정도 편집을 10분 뚝딱해서 가시성은 좀 안좋은 것 같다.. ㅎ

테스트 영상 미리보기

미디어 품질이 ultra -> high -> medium -> low -> very-low 로 내려가는데, medium일 때만 해상도 비율이 다른 것을 볼 수 있다. 해상도나 프레임률도 내려가긴 하지만 장치 자체가 좋은 장치가 아니라 티가 잘안나서 해상도 비율로 테스트를 했다.

결과와 느낀 점

결과

결과적으로는 사용자의 네트워크 품질이 낮을 때는 일부러 미디어의 품질을 낮추는 방식으로 오디오를 최대한 살리는 것이 중요하다고 느꼈다. 네트워크 품질을 모니터링하고 이걸 기반으로 어떤 constraints를 적용사용자 경험을 향상시킬 수 있었다.

느낀 점

네부캠 프로젝트를 하면서 멘토님이 기술적으로 고도화한다면 해볼만한 주제라고 해주셔서 이전부터 계획만 세워뒀었는데 이렇게 다 만들고 나니 정말 뿌듯하다.

다양한 상황을 고려한다라는 것 자체가 프론트엔드 개발에서 많이 중요한 것 같다고도 느꼈고, 네트워크 지표를 공부하면서 CS가 이렇게 쓰일 수 있겠구나 라는 인사이트를 체득할 수 있어서 좋았다.

이 기능을 만들기 위해서 네트워크 품질 지표 공부도 하고, 나름대로 계획도 세워서 구현했는데 원하는대로 잘 작동이 된 것 같아서 기분이 좋다.

기능을 구현하기 위한 학습을 했지만 네트워크의 다른 부분들도 알아두면 좋을 것 같은 내용들이 많이 있었기 때문에 더 공부해야할 것 같다고 느꼈다.

네트워크 기반 동적 품질 조절 기능 개발 END

📌 Table of Contents

0
추천 글